Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x 261x | /**
* @file devtools.js
* @description The browser side of `avenx serve --trace`.
*
* Recording is off unless a developer asks for it. The dev server injects one
* call to {@link installTraceRecorder} when `--trace` is passed, and nothing in
* this module runs otherwise — no listeners, no timers, no globals.
*
* What it adds is the smallest thing that closes the loop: start recording,
* let the developer reproduce the bug, then send the trace to the dev server so
* `avenx trace list` can see it. Everything after that happens in the terminal,
* where the rest of Avenx's tooling already lives.
* @module lib/core/trace/devtools
*/
import { startRecording, stopRecording, activeRecorder } from './recorder.js';
import { installRecordingGlobals, clearGlobalOverrides } from './globals.js';
import { tracer } from './tracer.js';
/**
* Where a recording is posted when the developer saves it.
* @type {string}
*/
export const TRACE_ENDPOINT = '/__avenx/trace';
/**
* The control surface published on `window.avenxTrace` while recording.
* @type {object|null}
*/
let controller = null;
/**
* Starts recording in a browser and publishes a small control surface.
*
* The recorder is armed after the current task rather than immediately, so the
* application's initial mount is treated as setup rather than as a session of
* unexplained state writes.
* @param {object} [options] - Recorder options.
* @param {string} [options.endpoint] - Where `save()` posts the trace.
* @param {string[]} [options.redact] - Property paths to withhold.
* @param {number} [options.maxNodes] - Ring-buffer capacity.
* @param {boolean} [options.autoSave] - Save automatically on page hide.
* @returns {object} The control surface, also published as `window.avenxTrace`.
*/
export function installTraceRecorder(options = {}) {
if (controller) {
return controller;
}
const endpoint = options.endpoint || TRACE_ENDPOINT;
const recorder = startRecording({
redact: options.redact || [],
maxNodes: options.maxNodes,
meta: {
url: typeof location !== 'undefined' ? location.href : undefined,
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
},
});
installRecordingGlobals(recorder);
// Mount happens synchronously after this call, so arming is deferred to the
// next macrotask. Writes made while a component builds itself are the app
// starting up, not a user interaction the trace can attribute.
setTimeout(() => {
if (activeRecorder() === recorder) {
recorder.arm();
}
}, 0);
/**
* Posts the current trace to the dev server.
* @returns {Promise<object>} `{ok, id}` or `{ok: false, error}`.
*/
const save = async () => {
const current = activeRecorder();
if (!current) {
return { ok: false, error: 'Recording has already stopped.' };
}
const trace = current.toJSON();
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trace),
});
if (!response.ok) {
return { ok: false, error: `Dev server responded ${response.status}` };
}
console.info(
`[Avenx] Saved ${trace.id} (${trace.nodes.length} nodes, ${trace.determinism.status}).\n` +
` avenx trace view ${trace.id}\n` +
` avenx trace export ${trace.id}`,
);
return { ok: true, id: trace.id };
} catch (error) {
return { ok: false, error: String((error && error.message) || error) };
}
};
controller = {
/** The id this recording will be saved under. */
get id() {
return recorder.id;
},
/** How many nodes have been recorded so far. */
get size() {
return recorder.nodes.length;
},
/** Whether the recording is still believed replayable. */
get deterministic() {
return recorder.isDeterministic;
},
save,
/** The trace as it stands, without saving it. */
snapshot: () => recorder.toJSON(),
/** Stops recording and restores the real globals. */
stop: () => {
clearGlobalOverrides();
controller = null;
return stopRecording();
},
};
if (typeof window !== 'undefined') {
window.avenxTrace = controller;
if (options.autoSave !== false && typeof document !== 'undefined') {
// `pagehide` rather than `beforeunload`: it fires for back/forward cache
// navigations too, which is exactly when a developer has finished
// reproducing something and clicked away.
window.addEventListener('pagehide', () => {
if (activeRecorder() === recorder && recorder.nodes.length > 0) {
// A keepalive beacon, because a normal fetch is cancelled on unload.
try {
const body = JSON.stringify(recorder.toJSON());
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' }));
}
} catch {
// Losing a trace on unload is not worth breaking navigation over.
}
}
});
}
console.info(
`[Avenx] Recording trace ${recorder.id}. Reproduce the behaviour, then run:\n` +
' await avenxTrace.save()\n' +
' (or just navigate away — the trace is sent automatically)',
);
}
return controller;
}
/**
* Stops any recording this module started.
* @returns {object|null} The finished trace.
*/
export function uninstallTraceRecorder() {
if (!controller) {
return null;
}
return controller.stop();
}
/**
* Whether a browser recording is currently running.
* @returns {boolean}
*/
export function isRecording() {
return controller !== null && tracer.on;
}
|