@alwaysmeticulous/backend-recorder-workerd 2.315.0 → 2.321.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 +10 -1
- package/dist/binding-patch.js +139 -0
- package/dist/binding-patch.js.map +1 -0
- package/dist/body-capture.d.ts +10 -1
- package/dist/body-capture.js +21 -4
- package/dist/body-capture.js.map +1 -1
- package/dist/context.d.ts +1 -0
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/fetch-patch.js +17 -103
- package/dist/fetch-patch.js.map +1 -1
- package/dist/index.d.ts +36 -12
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -1
- package/dist/kv-patch.js +261 -0
- package/dist/kv-patch.js.map +1 -0
- package/dist/original-fetch.js +22 -0
- package/dist/original-fetch.js.map +1 -0
- package/dist/{fetch-patch.d.ts → outbound-capture.d.ts} +2 -2
- package/dist/outbound-capture.js +127 -0
- package/dist/outbound-capture.js.map +1 -0
- package/dist/protocol.d.ts +134 -2
- package/dist/protocol.js +19 -3
- package/dist/protocol.js.map +1 -1
- package/dist/redact-body.d.ts +19 -0
- package/dist/redact-body.js +53 -0
- package/dist/redact-body.js.map +1 -0
- package/dist/replay-fetch.js +81 -0
- package/dist/replay-fetch.js.map +1 -0
- package/dist/replay-sidecar-url.js +62 -0
- package/dist/replay-sidecar-url.js.map +1 -0
- package/dist/sidecar-client.js +83 -4
- package/dist/sidecar-client.js.map +1 -1
- package/dist/virtual-clock.js +122 -0
- package/dist/virtual-clock.js.map +1 -0
- package/dist/virtual-random.js +238 -0
- package/dist/virtual-random.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
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]="3821aa29-b3ba-551f-957d-45a05bdc02bf")}catch(e){}}();
|
|
3
|
+
import { requestCaptureContext } from "./context.js";
|
|
4
|
+
import { warnOnce } from "./log.js";
|
|
5
|
+
//#region src/virtual-random.ts
|
|
6
|
+
/**
|
|
7
|
+
* Per-session randomness virtualization for replay mode.
|
|
8
|
+
*
|
|
9
|
+
* Mocking every outbound `fetch` is not enough to make a replay deterministic: a Worker that
|
|
10
|
+
* mints an id (`crypto.randomUUID()` for a guest id, `getRandomValues` for a token) produces
|
|
11
|
+
* a different value in the base replay than in the head replay, so the rendered HTML differs
|
|
12
|
+
* and any screenshot showing that value diffs forever. This replaces the worker's sources of
|
|
13
|
+
* randomness with seeded generators while serving a replayed request, so two replays of the
|
|
14
|
+
* same recorded session render identically.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately a *generator*, not a recording: like the browser replayer we do not capture
|
|
17
|
+
* the numbers the original session produced, we produce fresh ones from a fixed seed. The
|
|
18
|
+
* app still sees plausible random values, and two replays see the same ones.
|
|
19
|
+
*
|
|
20
|
+
* Ported from the Node backend recorder's `replay/virtual-random.ts`, which in turn mirrors
|
|
21
|
+
* the browser replayer's `mockOutRandomNumberGeneration`, including its butterfly-effect
|
|
22
|
+
* containment: a separate sequence per call stack, seeded from a per-call-site counter, so a
|
|
23
|
+
* change in one part of the app does not shift the numbers another part gets, and a shared
|
|
24
|
+
* helper called from two places does not return the same value twice. Two differences from
|
|
25
|
+
* the Node version, both forced by the runtime:
|
|
26
|
+
*
|
|
27
|
+
* - the session comes from the request context rather than a session resolver with a
|
|
28
|
+
* process-wide fallback, because workerd's AsyncLocalStorage propagates reliably from the
|
|
29
|
+
* inbound handler;
|
|
30
|
+
* - only the globals workerd actually has are patched — there is no `node:crypto` module to
|
|
31
|
+
* mutate.
|
|
32
|
+
*
|
|
33
|
+
* Reads outside a replayed request — module evaluation, a recording, a deployed production
|
|
34
|
+
* worker — fall through to the native generators untouched.
|
|
35
|
+
*/
|
|
36
|
+
const nativeMathRandom = Math.random.bind(Math);
|
|
37
|
+
const nativeRandomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);
|
|
38
|
+
const nativeGetRandomValues = globalThis.crypto?.getRandomValues?.bind(globalThis.crypto);
|
|
39
|
+
/**
|
|
40
|
+
* Frames of the stack that belong to this module and must be skipped to reach the app's call
|
|
41
|
+
* site, counted from the `new Error()` inside {@link getCallStack}.
|
|
42
|
+
*
|
|
43
|
+
* Getting this wrong degrades gracefully rather than breaking: a call site that resolves to
|
|
44
|
+
* one of our own frames merges sequences that should have been separate, which costs
|
|
45
|
+
* butterfly-effect isolation but is still perfectly deterministic.
|
|
46
|
+
*/
|
|
47
|
+
const FRAMES_TO_APP = 3;
|
|
48
|
+
/** How much of the stack identifies a sequence. Ten frames tells call paths apart cheaply. */
|
|
49
|
+
const STACK_FRAMES = 10;
|
|
50
|
+
/** Stack captures per session, after which sequences are keyed by call site alone. */
|
|
51
|
+
const MAX_STACK_CAPTURES = 1e4;
|
|
52
|
+
/** Bounds the state a stream of unrecognised session ids can accumulate in one isolate. */
|
|
53
|
+
const MAX_TRACKED_SESSIONS = 50;
|
|
54
|
+
const UNKNOWN_CALL_SITE = "unknown";
|
|
55
|
+
const stateBySession = /* @__PURE__ */ new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Alea, the same algorithm the browser replayer gets from `seedrandom`. Inlined because this
|
|
58
|
+
* package is published to npm and ships no runtime dependencies — a customer's Worker bundle
|
|
59
|
+
* should not grow for this.
|
|
60
|
+
*/
|
|
61
|
+
const createSeededRandom = (seed) => {
|
|
62
|
+
let n = 4022871197;
|
|
63
|
+
const mash = (data) => {
|
|
64
|
+
for (let i = 0; i < data.length; i++) {
|
|
65
|
+
n += data.charCodeAt(i);
|
|
66
|
+
let h = .02519603282416938 * n;
|
|
67
|
+
n = h >>> 0;
|
|
68
|
+
h -= n;
|
|
69
|
+
h *= n;
|
|
70
|
+
n = h >>> 0;
|
|
71
|
+
h -= n;
|
|
72
|
+
n += h * 4294967296;
|
|
73
|
+
}
|
|
74
|
+
return (n >>> 0) * 23283064365386963e-26;
|
|
75
|
+
};
|
|
76
|
+
let s0 = mash(" ");
|
|
77
|
+
let s1 = mash(" ");
|
|
78
|
+
let s2 = mash(" ");
|
|
79
|
+
let c = 1;
|
|
80
|
+
s0 -= mash(seed);
|
|
81
|
+
if (s0 < 0) s0 += 1;
|
|
82
|
+
s1 -= mash(seed);
|
|
83
|
+
if (s1 < 0) s1 += 1;
|
|
84
|
+
s2 -= mash(seed);
|
|
85
|
+
if (s2 < 0) s2 += 1;
|
|
86
|
+
return () => {
|
|
87
|
+
const t = 2091639 * s0 + c * 23283064365386963e-26;
|
|
88
|
+
s0 = s1;
|
|
89
|
+
s1 = s2;
|
|
90
|
+
c = t | 0;
|
|
91
|
+
s2 = t - c;
|
|
92
|
+
return s2;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const getCallStack = () => {
|
|
96
|
+
const originalLimit = Error.stackTraceLimit;
|
|
97
|
+
try {
|
|
98
|
+
Error.stackTraceLimit = FRAMES_TO_APP + STACK_FRAMES;
|
|
99
|
+
const stack = (/* @__PURE__ */ new Error()).stack;
|
|
100
|
+
if (stack == null) return {
|
|
101
|
+
stack: UNKNOWN_CALL_SITE,
|
|
102
|
+
site: UNKNOWN_CALL_SITE
|
|
103
|
+
};
|
|
104
|
+
const lines = stack.split("\n").slice(FRAMES_TO_APP + 1);
|
|
105
|
+
return {
|
|
106
|
+
stack: lines.join("\n") || UNKNOWN_CALL_SITE,
|
|
107
|
+
site: lines[0] ?? UNKNOWN_CALL_SITE
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return {
|
|
111
|
+
stack: UNKNOWN_CALL_SITE,
|
|
112
|
+
site: UNKNOWN_CALL_SITE
|
|
113
|
+
};
|
|
114
|
+
} finally {
|
|
115
|
+
Error.stackTraceLimit = originalLimit;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const getSessionState = () => {
|
|
119
|
+
const ctx = requestCaptureContext.getStore();
|
|
120
|
+
if (ctx === void 0 || ctx.mode !== "replay") return;
|
|
121
|
+
const existing = stateBySession.get(ctx.frontendSessionId);
|
|
122
|
+
if (existing !== void 0) return existing;
|
|
123
|
+
if (stateBySession.size >= MAX_TRACKED_SESSIONS) {
|
|
124
|
+
const oldest = stateBySession.keys().next();
|
|
125
|
+
if (!oldest.done) stateBySession.delete(oldest.value);
|
|
126
|
+
}
|
|
127
|
+
const created = {
|
|
128
|
+
frontendSessionId: ctx.frontendSessionId,
|
|
129
|
+
generatorBySequenceKey: /* @__PURE__ */ new Map(),
|
|
130
|
+
nextCallerIdByCallSite: /* @__PURE__ */ new Map(),
|
|
131
|
+
stackCaptures: 0
|
|
132
|
+
};
|
|
133
|
+
stateBySession.set(ctx.frontendSessionId, created);
|
|
134
|
+
return created;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* The generator for the sequence the current call belongs to, or undefined outside a
|
|
138
|
+
* replayed request.
|
|
139
|
+
*
|
|
140
|
+
* Sequences are keyed by the whole call stack, so a second call down the same path continues
|
|
141
|
+
* the same sequence and gets a different value. The *seed* comes from a counter kept per
|
|
142
|
+
* immediate call site, which is what keeps a shared id helper honest: two call paths through
|
|
143
|
+
* the same `generateId()` share a call site, so they are seeded 0 and 1 and return different
|
|
144
|
+
* ids.
|
|
145
|
+
*
|
|
146
|
+
* Note what the seed deliberately does NOT contain: any part of the stack. Base and head are
|
|
147
|
+
* different builds of the app, so the same logical call site has a different string in each —
|
|
148
|
+
* a seed built from it would change on every rebuild. The consequence, which the browser
|
|
149
|
+
* replayer shares, is that two *distinct* call sites each seen for the first time both seed at
|
|
150
|
+
* 0 and hand out the same first value. That is the accepted trade: an alternative that
|
|
151
|
+
* separated them (say, an ordinal per call site) would shift every later site's values as soon
|
|
152
|
+
* as a PR added one randomness call, which is the butterfly effect this whole scheme exists to
|
|
153
|
+
* avoid.
|
|
154
|
+
*/
|
|
155
|
+
const openSequence = () => {
|
|
156
|
+
const state = getSessionState();
|
|
157
|
+
if (state === void 0) return;
|
|
158
|
+
let sequenceKey = UNKNOWN_CALL_SITE;
|
|
159
|
+
let callSite = UNKNOWN_CALL_SITE;
|
|
160
|
+
if (state.stackCaptures < MAX_STACK_CAPTURES) {
|
|
161
|
+
state.stackCaptures++;
|
|
162
|
+
const captured = getCallStack();
|
|
163
|
+
sequenceKey = captured.stack;
|
|
164
|
+
callSite = captured.site;
|
|
165
|
+
}
|
|
166
|
+
const existing = state.generatorBySequenceKey.get(sequenceKey);
|
|
167
|
+
if (existing !== void 0) return existing;
|
|
168
|
+
const callerId = state.nextCallerIdByCallSite.get(callSite) ?? 0;
|
|
169
|
+
const generator = createSeededRandom(`meticulous-random-${state.frontendSessionId}-${callerId}`);
|
|
170
|
+
state.generatorBySequenceKey.set(sequenceKey, generator);
|
|
171
|
+
state.nextCallerIdByCallSite.set(callSite, callerId + 1);
|
|
172
|
+
return generator;
|
|
173
|
+
};
|
|
174
|
+
const HEX = "0123456789abcdef";
|
|
175
|
+
/** The two bits of a v4 UUID's variant field are fixed, leaving these four values. */
|
|
176
|
+
const VARIANT = "89ab";
|
|
177
|
+
const buildUuid = (draw) => {
|
|
178
|
+
let uuid = "";
|
|
179
|
+
for (let i = 0; i < 36; i++) if (i === 8 || i === 13 || i === 18 || i === 23) uuid += "-";
|
|
180
|
+
else if (i === 14) uuid += "4";
|
|
181
|
+
else if (i === 19) uuid += VARIANT[Math.floor(draw() * 4)];
|
|
182
|
+
else uuid += HEX[Math.floor(draw() * 16)];
|
|
183
|
+
return uuid;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Fills the view's bytes rather than its elements, so every typed array — including the
|
|
187
|
+
* BigInt ones, whose elements cannot be assigned a number — is handled by one path.
|
|
188
|
+
*/
|
|
189
|
+
const fillBytes = (view, draw) => {
|
|
190
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
191
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(draw() * 256);
|
|
192
|
+
};
|
|
193
|
+
let installed = false;
|
|
194
|
+
/**
|
|
195
|
+
* Replaces the worker's sources of randomness with per-session seeded generators. Idempotent,
|
|
196
|
+
* and a no-op failure: a worker that will not let them be replaced still replays, just
|
|
197
|
+
* without randomness virtualisation.
|
|
198
|
+
*
|
|
199
|
+
* Called lazily from the replay branch of the request wrapper rather than at module scope,
|
|
200
|
+
* for the same reasons as the virtual clock: record mode and deployed production workers then
|
|
201
|
+
* pay nothing, and the package declares no `sideEffects`, so a module-scope side effect could
|
|
202
|
+
* legitimately be tree-shaken away by a customer's bundler.
|
|
203
|
+
*/
|
|
204
|
+
const installVirtualRandom = () => {
|
|
205
|
+
if (installed) return;
|
|
206
|
+
installed = true;
|
|
207
|
+
try {
|
|
208
|
+
Math.random = () => openSequence()?.() ?? nativeMathRandom();
|
|
209
|
+
if (globalThis.crypto != null) {
|
|
210
|
+
Object.defineProperty(globalThis.crypto, "randomUUID", {
|
|
211
|
+
value: () => {
|
|
212
|
+
const draw = openSequence();
|
|
213
|
+
if (draw !== void 0) return buildUuid(draw);
|
|
214
|
+
return nativeRandomUUID?.() ?? buildUuid(nativeMathRandom);
|
|
215
|
+
},
|
|
216
|
+
configurable: true,
|
|
217
|
+
writable: true
|
|
218
|
+
});
|
|
219
|
+
Object.defineProperty(globalThis.crypto, "getRandomValues", {
|
|
220
|
+
value: (array) => {
|
|
221
|
+
const draw = openSequence();
|
|
222
|
+
if (draw === void 0) return nativeGetRandomValues?.(array) ?? array;
|
|
223
|
+
fillBytes(array, draw);
|
|
224
|
+
return array;
|
|
225
|
+
},
|
|
226
|
+
configurable: true,
|
|
227
|
+
writable: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
warnOnce("virtual-random", "Could not install the Meticulous replay random-number generators — ids minted by the app will differ between replays.", error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
export { installVirtualRandom };
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=virtual-random.js.map
|
|
238
|
+
//# debugId=3821aa29-b3ba-551f-957d-45a05bdc02bf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtual-random.js","sources":["../src/virtual-random.ts"],"sourcesContent":["import { requestCaptureContext } from \"./context\";\nimport { warnOnce } from \"./log\";\n\n/**\n * Per-session randomness virtualization for replay mode.\n *\n * Mocking every outbound `fetch` is not enough to make a replay deterministic: a Worker that\n * mints an id (`crypto.randomUUID()` for a guest id, `getRandomValues` for a token) produces\n * a different value in the base replay than in the head replay, so the rendered HTML differs\n * and any screenshot showing that value diffs forever. This replaces the worker's sources of\n * randomness with seeded generators while serving a replayed request, so two replays of the\n * same recorded session render identically.\n *\n * Deliberately a *generator*, not a recording: like the browser replayer we do not capture\n * the numbers the original session produced, we produce fresh ones from a fixed seed. The\n * app still sees plausible random values, and two replays see the same ones.\n *\n * Ported from the Node backend recorder's `replay/virtual-random.ts`, which in turn mirrors\n * the browser replayer's `mockOutRandomNumberGeneration`, including its butterfly-effect\n * containment: a separate sequence per call stack, seeded from a per-call-site counter, so a\n * change in one part of the app does not shift the numbers another part gets, and a shared\n * helper called from two places does not return the same value twice. Two differences from\n * the Node version, both forced by the runtime:\n *\n * - the session comes from the request context rather than a session resolver with a\n * process-wide fallback, because workerd's AsyncLocalStorage propagates reliably from the\n * inbound handler;\n * - only the globals workerd actually has are patched — there is no `node:crypto` module to\n * mutate.\n *\n * Reads outside a replayed request — module evaluation, a recording, a deployed production\n * worker — fall through to the native generators untouched.\n */\n\nconst nativeMathRandom = Math.random.bind(Math);\nconst nativeRandomUUID = globalThis.crypto?.randomUUID?.bind(globalThis.crypto);\nconst nativeGetRandomValues = globalThis.crypto?.getRandomValues?.bind(\n globalThis.crypto,\n);\n\n/**\n * Frames of the stack that belong to this module and must be skipped to reach the app's call\n * site, counted from the `new Error()` inside {@link getCallStack}.\n *\n * Getting this wrong degrades gracefully rather than breaking: a call site that resolves to\n * one of our own frames merges sequences that should have been separate, which costs\n * butterfly-effect isolation but is still perfectly deterministic.\n */\nconst FRAMES_TO_APP = 3; // getCallStack ← openSequence ← patched global ← app\n\n/** How much of the stack identifies a sequence. Ten frames tells call paths apart cheaply. */\nconst STACK_FRAMES = 10;\n\n/** Stack captures per session, after which sequences are keyed by call site alone. */\nconst MAX_STACK_CAPTURES = 10_000;\n\n/** Bounds the state a stream of unrecognised session ids can accumulate in one isolate. */\nconst MAX_TRACKED_SESSIONS = 50;\n\nconst UNKNOWN_CALL_SITE = \"unknown\";\n\ninterface SessionRandomState {\n frontendSessionId: string;\n generatorBySequenceKey: Map<string, () => number>;\n nextCallerIdByCallSite: Map<string, number>;\n stackCaptures: number;\n}\n\nconst stateBySession = new Map<string, SessionRandomState>();\n\n// ---------------------------------------------------------------------------\n// Seeded PRNG (Alea)\n// ---------------------------------------------------------------------------\n\n/**\n * Alea, the same algorithm the browser replayer gets from `seedrandom`. Inlined because this\n * package is published to npm and ships no runtime dependencies — a customer's Worker bundle\n * should not grow for this.\n */\nexport const createSeededRandom = (seed: string): (() => number) => {\n let n = 0xefc8249d;\n const mash = (data: string): number => {\n for (let i = 0; i < data.length; i++) {\n n += data.charCodeAt(i);\n let h = 0.02519603282416938 * n;\n n = h >>> 0;\n h -= n;\n h *= n;\n n = h >>> 0;\n h -= n;\n n += h * 0x100000000;\n }\n return (n >>> 0) * 2.3283064365386963e-10;\n };\n\n let s0 = mash(\" \");\n let s1 = mash(\" \");\n let s2 = mash(\" \");\n let c = 1;\n s0 -= mash(seed);\n if (s0 < 0) {\n s0 += 1;\n }\n s1 -= mash(seed);\n if (s1 < 0) {\n s1 += 1;\n }\n s2 -= mash(seed);\n if (s2 < 0) {\n s2 += 1;\n }\n\n return () => {\n const t = 2091639 * s0 + c * 2.3283064365386963e-10;\n s0 = s1;\n s1 = s2;\n c = t | 0;\n s2 = t - c;\n return s2;\n };\n};\n\n// ---------------------------------------------------------------------------\n// Sequence resolution\n// ---------------------------------------------------------------------------\n\nconst getCallStack = (): { stack: string; site: string } => {\n const originalLimit = Error.stackTraceLimit;\n try {\n Error.stackTraceLimit = FRAMES_TO_APP + STACK_FRAMES;\n const stack = new Error().stack;\n if (stack == null) {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n }\n // Line 0 is the \"Error\" header; frame lines follow, innermost first, and the first\n // FRAMES_TO_APP of them are ours.\n const lines = stack.split(\"\\n\").slice(FRAMES_TO_APP + 1);\n return {\n stack: lines.join(\"\\n\") || UNKNOWN_CALL_SITE,\n site: lines[0] ?? UNKNOWN_CALL_SITE,\n };\n } catch {\n return { stack: UNKNOWN_CALL_SITE, site: UNKNOWN_CALL_SITE };\n } finally {\n Error.stackTraceLimit = originalLimit;\n }\n};\n\nconst getSessionState = (): SessionRandomState | undefined => {\n const ctx = requestCaptureContext.getStore();\n if (ctx === undefined || ctx.mode !== \"replay\") {\n return undefined;\n }\n const existing = stateBySession.get(ctx.frontendSessionId);\n if (existing !== undefined) {\n return existing;\n }\n if (stateBySession.size >= MAX_TRACKED_SESSIONS) {\n const oldest = stateBySession.keys().next();\n if (!oldest.done) {\n stateBySession.delete(oldest.value);\n }\n }\n const created: SessionRandomState = {\n frontendSessionId: ctx.frontendSessionId,\n generatorBySequenceKey: new Map(),\n nextCallerIdByCallSite: new Map(),\n stackCaptures: 0,\n };\n stateBySession.set(ctx.frontendSessionId, created);\n return created;\n};\n\n/**\n * The generator for the sequence the current call belongs to, or undefined outside a\n * replayed request.\n *\n * Sequences are keyed by the whole call stack, so a second call down the same path continues\n * the same sequence and gets a different value. The *seed* comes from a counter kept per\n * immediate call site, which is what keeps a shared id helper honest: two call paths through\n * the same `generateId()` share a call site, so they are seeded 0 and 1 and return different\n * ids.\n *\n * Note what the seed deliberately does NOT contain: any part of the stack. Base and head are\n * different builds of the app, so the same logical call site has a different string in each —\n * a seed built from it would change on every rebuild. The consequence, which the browser\n * replayer shares, is that two *distinct* call sites each seen for the first time both seed at\n * 0 and hand out the same first value. That is the accepted trade: an alternative that\n * separated them (say, an ordinal per call site) would shift every later site's values as soon\n * as a PR added one randomness call, which is the butterfly effect this whole scheme exists to\n * avoid.\n */\nconst openSequence = (): (() => number) | undefined => {\n const state = getSessionState();\n if (state === undefined) {\n return undefined;\n }\n\n let sequenceKey = UNKNOWN_CALL_SITE;\n let callSite = UNKNOWN_CALL_SITE;\n if (state.stackCaptures < MAX_STACK_CAPTURES) {\n state.stackCaptures++;\n const captured = getCallStack();\n sequenceKey = captured.stack;\n callSite = captured.site;\n }\n\n const existing = state.generatorBySequenceKey.get(sequenceKey);\n if (existing !== undefined) {\n return existing;\n }\n const callerId = state.nextCallerIdByCallSite.get(callSite) ?? 0;\n // Seeded from the counter only — never from the call-site or stack string itself. Those embed\n // bundle filenames and line numbers, which differ between the base build and the head build\n // of the very same code, so putting one in the seed would reseed every sequence on any\n // rebuild and bring back exactly the diffs this exists to remove. The strings are grouping\n // keys, nothing more.\n const generator = createSeededRandom(\n `meticulous-random-${state.frontendSessionId}-${callerId}`,\n );\n state.generatorBySequenceKey.set(sequenceKey, generator);\n state.nextCallerIdByCallSite.set(callSite, callerId + 1);\n return generator;\n};\n\n// ---------------------------------------------------------------------------\n// Derived primitives\n// ---------------------------------------------------------------------------\n\nconst HEX = \"0123456789abcdef\";\n/** The two bits of a v4 UUID's variant field are fixed, leaving these four values. */\nconst VARIANT = \"89ab\";\n\nconst buildUuid = (draw: () => number): string => {\n let uuid = \"\";\n for (let i = 0; i < 36; i++) {\n if (i === 8 || i === 13 || i === 18 || i === 23) {\n uuid += \"-\";\n } else if (i === 14) {\n uuid += \"4\";\n } else if (i === 19) {\n uuid += VARIANT[Math.floor(draw() * VARIANT.length)];\n } else {\n uuid += HEX[Math.floor(draw() * HEX.length)];\n }\n }\n return uuid;\n};\n\n/**\n * Fills the view's bytes rather than its elements, so every typed array — including the\n * BigInt ones, whose elements cannot be assigned a number — is handled by one path.\n */\nconst fillBytes = (view: ArrayBufferView, draw: () => number): void => {\n const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = Math.floor(draw() * 256);\n }\n};\n\n// ---------------------------------------------------------------------------\n// Install\n// ---------------------------------------------------------------------------\n\nlet installed = false;\n\n/**\n * Replaces the worker's sources of randomness with per-session seeded generators. Idempotent,\n * and a no-op failure: a worker that will not let them be replaced still replays, just\n * without randomness virtualisation.\n *\n * Called lazily from the replay branch of the request wrapper rather than at module scope,\n * for the same reasons as the virtual clock: record mode and deployed production workers then\n * pay nothing, and the package declares no `sideEffects`, so a module-scope side effect could\n * legitimately be tree-shaken away by a customer's bundler.\n */\nexport const installVirtualRandom = (): void => {\n if (installed) {\n return;\n }\n installed = true;\n try {\n Math.random = () => openSequence()?.() ?? nativeMathRandom();\n\n if (globalThis.crypto != null) {\n Object.defineProperty(globalThis.crypto, \"randomUUID\", {\n value: (): string => {\n const draw = openSequence();\n if (draw !== undefined) {\n return buildUuid(draw);\n }\n return nativeRandomUUID?.() ?? buildUuid(nativeMathRandom);\n },\n configurable: true,\n writable: true,\n });\n\n Object.defineProperty(globalThis.crypto, \"getRandomValues\", {\n value: <T extends ArrayBufferView>(array: T): T => {\n const draw = openSequence();\n if (draw === undefined) {\n return nativeGetRandomValues?.(array) ?? array;\n }\n fillBytes(array, draw);\n return array;\n },\n configurable: true,\n writable: true,\n });\n }\n } catch (error) {\n warnOnce(\n \"virtual-random\",\n \"Could not install the Meticulous replay random-number generators — ids minted by the app will differ between replays.\",\n error,\n );\n }\n};\n"],"names":[],"mappings":"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","debugId":"3821aa29-b3ba-551f-957d-45a05bdc02bf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwaysmeticulous/backend-recorder-workerd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.321.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",
|