What a stack string contains
When a script creates an Error, the engine records the call stack and exposes it as a string. The engines differ in almost every detail:
| Engine | Frame format | Details |
|---|---|---|
| V8 | at Object.render (https://a.example/app.js:12:9) | First line Error; methods named by call form; Error.stackTraceLimit 10; URL keeps the query |
| JavaScriptCore | render@https://a.example/app.js:12:15 | No header; named by definition; query and fragment stripped; strict-mode tail calls can drop frames |
| SpiderMonkey | render@https://a.example/app.js:12:9 | Eval and Function frames marked as line N > eval |
A single stack string therefore names the engine, and the exact column numbers show which engine's position rules produced it. A User-Agent override cannot change how V8 prints a frame. This is one of the checks behind engine oracle fingerprinting; it is a different signal from stack depth fingerprinting, which measures how deep recursion can go.
What automation leaves in stacks
Measured in Chrome 156, code run by common drivers produces distinctive frames:
- Puppeteer
page.evaluate: frames atpptr:evaluate;%2Fpath%2Fto%2Fscript.js%3A20%3A33:1:8. Puppeteer appends a//# sourceURL=comment, and the URL encodes the driver script's absolute path, line and column on the controlling machine. - Playwright
page.evaluate:at UtilityScript.evaluate (<anonymous>:372:16)and aneval at evaluateframe, because Playwright runs user code through an evaluator class. - Raw protocol evaluation and init scripts: frames like
at <anonymous>:1:35with no URL. Real page scripts always have a URL.
These frames only show up if the injected code is on the stack when a sample is taken. Timers and event callbacks start with a fresh stack: a setTimeout callback had exactly one frame in testing.
When widgets sample the stack
One widely deployed challenge loader records new Error().stack during its render call. If the widget is declared in the page's markup, the loader renders it from its own DOM-ready handler and the stack contains only loader frames. If a page script calls the render function, the stack contains that script's URL, line, column and function names, and if an evaluated driver script makes the call, the driver's frames are recorded too. See what a challenge widget collects.
The same widget samples the stack again inside its checkbox click handler. That stack has to look like the claimed engine's: V8 names the listener frame HTMLInputElement.<anonymous>, JavaScriptCore leaves many frames anonymous. A Safari User-Agent with a V8-shaped stack contradicts the session.
Two practical points follow: let the page render its own widgets instead of calling page APIs from injected code, and run the engine you claim, because the stack format belongs to the engine. A managed web data API that drives real browsers without injecting page-world code avoids both problems.
