Methodology
How the rest of these pages were measured, and the traps that produced wrong answers first.
Run the script, do not read it
Both generations flatten control flow into numeric state machines and decode their strings at runtime with a stateful decoder. Static reading stalls immediately: d4[f()][g()] means nothing until you know that f() returned "navigator" and g() returned "userAgent" on this run.
So the script is instrumented as served, with every assignment, return value, array element and decoder call wrapped in a recorder that keeps the last few values per site plus a global sequence number, and the table is snapshotted at the moment the run builds its first payload. A field is then explained by walking its expression backwards: an identifier resolves to the assignment that last wrote it, a call resolves to the return that produced the value the caller received, a member access resolves through the recorded decoder call that produced its key.
Two rules make this work and are easy to get wrong:
- Do not fold the strings first. Replacing decoder calls with their results changes what later calls return, because the decoder is stateful. The folded copy is for reading, never for running.
- Preserve the integrity hash. The instrumented source is handed back to the script through a patched
Function.prototype.toStringthat strips the recorder wrappers, so the script's own hash still matches. Two bugs in that path silently corrupted whole runs: an unwrapper that tracked string quotes but not regex literals mangled everything after a/\\|"/gin the source, and wrappingreturn a, b, casrecord(7, a, b, c)turned the comma operator into argument separators and quietly replaced a dispatcher's return value.
Compare payloads field by field
The sandbox payload is compared against the payload the real page posted in the same session, with the snapshot taken at the moment the page makes its first post rather than at the end of the wait. That timing matters: a page grows form inputs while it sits, so a late snapshot makes the form inventory fields differ for reasons that have nothing to do with the sandbox.
Every field lands in one bucket:
| bucket | meaning |
|---|---|
match | identical in both |
volatile | differs, and differs between two real posts of one real session too |
gap | differs, and a real browser holds it steady |
missing | the real payload has it, ours does not |
extra | ours has it, the real payload does not |
Only the last three are defects. The volatile list is not an opinion: it is generated by comparing two real posts of one session and taking exactly the fields a browser does not hold steady. Matching is on the leaf name rather than the full path, because different builds nest the same field under different parents.
Separate the signal from the noise with a second baseline
For anything measured by changing one environment fact and diffing payloads, the baseline is run twice and every address that differs between two identical clean runs is dropped. On one property that filter removes 18 to 21 addresses per sweep, mostly timings, the derived block, the hour bucket and the session counters.
That filter is only good enough to read a single-bit mask change through. Any result that depends on a smaller difference needs deterministic replay rather than another live sweep.
Charge the sandbox for the time it did not spend
A sandbox that runs the sensor outside a browser runs it too fast, and the payload says so: fields carrying elapsed milliseconds from bmak.startTs read 2 to 5 where a real browser reads 24 to 31.
Two measurements settled what that gap is. A CPU profile of a real load shows the script holding the CPU for about 37 ms before the builder runs and another 37 ms inside it, so the time is real compute rather than waiting. Instrumenting the same script in the browser and in the sandbox and counting calls up to the moment of the post gives 587,745 against 586,620, a difference of 0.2 percent. The sandbox is not skipping work, it runs the same code an order of magnitude faster.
Scaling the clock cannot fix that, because the two windows are wrong in opposite directions. What works is charging the initialisation cost once, at the moment the script writes bmak.startTs, and letting everything after that run at the sandbox's own speed:
export const DEFAULT_INIT_COST = 25;
export const DEFAULT_FRICTION = 0.12;
With the charge in place the elapsed fields land on the real values. This is a calibration measured from real loads, not a simulation, and it says nothing about how a different machine would time the same work.
Keep an offline regression suite
Anything learned about a build is pinned as a capture: the script bytes, the environment snapshot, the real payloads and the metadata. The checks then run with no browser and no network, and fail if a single non-volatile field drifts. What they cover, per pin:
an adapter claims every pinned payload
an unknown build is reported rather than misparsed
every pinned payload decodes and re-encodes byte for byte
every pinned payload's checksum matches
the recovered shuffle key reproduces the permutation
the decoded plaintext is well formed for this build
the shuffle and its inverse agree on random text
a known permutation is recovered from its own key space
script parses and its features are readable
integrity hash reproduces the build's delta
re-signing an edited script keeps the delta
murmur port agrees with itself on a known string
the preamble token derives from the timestamp alone
a payload rebuilt from its own template is identical
an overridden field survives a forge and decode
the captured derived block factorises its own values
Ten pins across both generations currently pass 21 to 23 checks each, with the rest reported as not applicable to that generation rather than silently skipped. The value of this is not the checks themselves, it is that a claim made once stays true after the next change, and that a stale pin fails loudly when a clock bucket ages out rather than being quietly tolerated everywhere.
Attribute failures to one layer at a time
Two things are being tested whenever a payload is judged: the payload and the handshake. Send a payload from inside a real browser to test the payload. Send a known-good payload from your own client to test the handshake. Doing both at once produces a result nobody can act on, and a payload that fails because of a socket looks exactly like a payload that fails because of a field.