@jsenv/core 41.4.1 → 41.4.2
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/build/build.js +173 -4
- package/dist/build/jsenv_core_packages.js +91 -1
- package/dist/html/client_monitor_page.html +4 -1
- package/dist/html/clients_page.html +13 -5
- package/dist/js/client_reporter.js +185 -109
- package/dist/js/page_switcher.js +731 -0
- package/dist/start_dev_server/jsenv_core_packages.js +695 -4
- package/dist/start_dev_server/start_dev_server.js +357 -78
- package/package.json +2 -2
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +55 -5
- package/src/dev/start_dev_server.js +13 -3
- package/src/kitchen/kitchen.js +12 -3
- package/src/kitchen/url_graph/url_graph.js +11 -0
- package/src/plugins/client_monitoring/client/client_monitor_page.html +4 -1
- package/src/plugins/client_monitoring/client/client_reporter.js +185 -109
- package/src/plugins/client_monitoring/client/clients_page.html +13 -5
- package/src/plugins/client_monitoring/jsenv_plugin_client_monitoring.js +76 -68
- package/src/plugins/page_switcher/client/page_switcher.js +731 -0
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +42 -0
- package/src/plugins/protocol_file/html_pages.js +122 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +24 -0
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +5 -0
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
* - buffered console logs
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
// The main client is the machine the dev server runs on, talking to itself
|
|
17
|
+
// via localhost. Watching it makes no sense — its devtools are one keystroke
|
|
18
|
+
// away, and the person reading the dashboard IS this client — so it only
|
|
19
|
+
// reports presence (heartbeat + tab info, so the dashboard lists it and can
|
|
20
|
+
// pilot it) and watches nothing: no console capture, no activity tracking, no
|
|
21
|
+
// fetch/history patching. A client reaching the server over the network (a
|
|
22
|
+
// phone on the LAN ip, acceptAnyIp: true) is the one being monitored, and
|
|
23
|
+
// reports everything.
|
|
24
|
+
const LOCAL_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
25
|
+
const isLocalClient = LOCAL_HOSTNAMES.includes(window.location.hostname);
|
|
26
|
+
|
|
16
27
|
const CLIENT_ID_STORAGE_KEY = "jsenv_client_id";
|
|
17
28
|
const TAB_ID_STORAGE_KEY = "jsenv_tab_id";
|
|
18
29
|
const REPORT_ENDPOINT = "/.internal/clients/report";
|
|
@@ -25,6 +36,12 @@ const HEARTBEAT_MS = 15000;
|
|
|
25
36
|
// Continuous activities (mousemove, scroll) only need to refresh "what the tab
|
|
26
37
|
// is doing" occasionally, not on every event.
|
|
27
38
|
const CONTINUOUS_THROTTLE_MS = 2000;
|
|
39
|
+
// A single log line is worth reading, not storing whole: one console.log of a
|
|
40
|
+
// big object serializes to megabytes, and that string is then kept by the
|
|
41
|
+
// buffer here, by the dev server's own per-client buffer, and by the
|
|
42
|
+
// server-events history — three copies of something nobody will read past the
|
|
43
|
+
// first screen. Cut at the source, once, where the size is known.
|
|
44
|
+
const LOG_TEXT_MAX = 10_000;
|
|
28
45
|
|
|
29
46
|
const randomId = () =>
|
|
30
47
|
typeof window.crypto !== "undefined" && window.crypto.randomUUID
|
|
@@ -191,19 +208,47 @@ const setup = () => {
|
|
|
191
208
|
}
|
|
192
209
|
};
|
|
193
210
|
|
|
211
|
+
// A page can declare itself perf critical (window.__jsenv_perf_critical__()):
|
|
212
|
+
// measuring an animation means nothing if the thing measuring it steals a
|
|
213
|
+
// frame. Monitoring then holds everything back until the page has been
|
|
214
|
+
// genuinely idle — no interaction for a while — instead of flushing on its
|
|
215
|
+
// own schedule.
|
|
216
|
+
let perfCritical = false;
|
|
217
|
+
let lastInteractionMs = 0;
|
|
218
|
+
const PERF_CRITICAL_QUIET_MS = 2000;
|
|
219
|
+
const isQuiet = () =>
|
|
220
|
+
!perfCritical || Date.now() - lastInteractionMs > PERF_CRITICAL_QUIET_MS;
|
|
221
|
+
const onInteraction = () => {
|
|
222
|
+
lastInteractionMs = Date.now();
|
|
223
|
+
};
|
|
224
|
+
if (!isLocalClient) {
|
|
225
|
+
for (const eventName of ["pointerdown", "keydown", "wheel", "touchstart"]) {
|
|
226
|
+
window.addEventListener(eventName, onInteraction, {
|
|
227
|
+
capture: true,
|
|
228
|
+
passive: true,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
194
233
|
let flushScheduled = false;
|
|
195
234
|
const scheduleFlush = () => {
|
|
196
235
|
if (flushScheduled) {
|
|
197
236
|
return;
|
|
198
237
|
}
|
|
199
238
|
flushScheduled = true;
|
|
200
|
-
|
|
239
|
+
const attempt = () => {
|
|
240
|
+
if (!isQuiet()) {
|
|
241
|
+
// Still being used: come back later rather than take the frame now.
|
|
242
|
+
setTimeout(attempt, PERF_CRITICAL_QUIET_MS);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
201
245
|
flushScheduled = false;
|
|
202
246
|
if (!pendingLogs.length && !pendingActivities.length) {
|
|
203
247
|
return;
|
|
204
248
|
}
|
|
205
249
|
post();
|
|
206
|
-
}
|
|
250
|
+
};
|
|
251
|
+
setTimeout(attempt, FLUSH_INTERVAL_MS);
|
|
207
252
|
};
|
|
208
253
|
|
|
209
254
|
const pushLog = (entry) => {
|
|
@@ -224,15 +269,35 @@ const setup = () => {
|
|
|
224
269
|
let consoleProcessScheduled = false;
|
|
225
270
|
// requestIdleCallback is missing on Safari/iOS (our main mobile target), so
|
|
226
271
|
// fall back to setTimeout there; either way each run is time-boxed below.
|
|
272
|
+
// Named on window rather than exported: the page that needs it is a plain
|
|
273
|
+
// html file, and it must be able to ask before anything else has loaded.
|
|
274
|
+
window.__jsenv_perf_critical__ = () => {
|
|
275
|
+
perfCritical = true;
|
|
276
|
+
};
|
|
277
|
+
|
|
227
278
|
const scheduleIdle =
|
|
228
279
|
typeof requestIdleCallback === "function"
|
|
229
280
|
? (fn) => requestIdleCallback(fn, { timeout: 1000 })
|
|
230
281
|
: (fn) => setTimeout(fn, 0);
|
|
282
|
+
const truncate = (text) =>
|
|
283
|
+
typeof text === "string" && text.length > LOG_TEXT_MAX
|
|
284
|
+
? `${text.slice(0, LOG_TEXT_MAX)}… (${text.length - LOG_TEXT_MAX} more characters)`
|
|
285
|
+
: text;
|
|
231
286
|
const formatOne = (captured) => {
|
|
287
|
+
const formatted = formatConsole(captured.args);
|
|
232
288
|
pushLog({
|
|
233
289
|
level: captured.level,
|
|
234
290
|
ts: captured.ts,
|
|
235
|
-
...
|
|
291
|
+
...formatted,
|
|
292
|
+
text: truncate(formatted.text),
|
|
293
|
+
// The styled runs carry the same text a second time (see formatConsole),
|
|
294
|
+
// so they are cut the same way.
|
|
295
|
+
segments: formatted.segments
|
|
296
|
+
? formatted.segments.map((segment) => ({
|
|
297
|
+
...segment,
|
|
298
|
+
text: truncate(segment.text),
|
|
299
|
+
}))
|
|
300
|
+
: undefined,
|
|
236
301
|
});
|
|
237
302
|
};
|
|
238
303
|
const processConsoleQueue = (deadline) => {
|
|
@@ -281,122 +346,129 @@ const setup = () => {
|
|
|
281
346
|
scheduleFlush();
|
|
282
347
|
};
|
|
283
348
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
349
|
+
// Everything that WATCHES the page — console, errors, activity, requests,
|
|
350
|
+
// navigations — is what the main client does without (see isLocalClient).
|
|
351
|
+
if (!isLocalClient) {
|
|
352
|
+
// Forward console.* while keeping the original behavior intact.
|
|
353
|
+
const LEVELS = ["log", "info", "warn", "error", "debug"];
|
|
354
|
+
for (const level of LEVELS) {
|
|
355
|
+
const original = console[level];
|
|
356
|
+
if (typeof original !== "function") {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
console[level] = (...args) => {
|
|
360
|
+
original.apply(console, args);
|
|
361
|
+
captureConsole(level, args);
|
|
362
|
+
};
|
|
290
363
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
window.addEventListener("unhandledrejection", (event) => {
|
|
303
|
-
pushLog({
|
|
304
|
-
level: "error",
|
|
305
|
-
text: `Unhandled rejection: ${formatArg(event.reason)}`,
|
|
364
|
+
window.addEventListener("error", (event) => {
|
|
365
|
+
const location = event.filename
|
|
366
|
+
? ` (${event.filename}:${event.lineno})`
|
|
367
|
+
: "";
|
|
368
|
+
pushLog({ level: "error", text: `${event.message}${location}` });
|
|
369
|
+
});
|
|
370
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
371
|
+
pushLog({
|
|
372
|
+
level: "error",
|
|
373
|
+
text: `Unhandled rejection: ${formatArg(event.reason)}`,
|
|
374
|
+
});
|
|
306
375
|
});
|
|
307
|
-
});
|
|
308
376
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
377
|
+
// Qualified activity so the dashboard can say what the tab was last doing.
|
|
378
|
+
// The detail is kept short enough to read inline (e.g. "mousemove: 40/120").
|
|
379
|
+
window.addEventListener(
|
|
380
|
+
"click",
|
|
381
|
+
(event) => recordActivity("click", `${event.clientX}/${event.clientY}`),
|
|
382
|
+
{ passive: true },
|
|
383
|
+
);
|
|
384
|
+
window.addEventListener(
|
|
385
|
+
"keydown",
|
|
386
|
+
(event) => recordActivity("keydown", event.key),
|
|
387
|
+
{ passive: true },
|
|
388
|
+
);
|
|
389
|
+
let lastMove = 0;
|
|
390
|
+
let lastScroll = 0;
|
|
391
|
+
window.addEventListener(
|
|
392
|
+
"mousemove",
|
|
393
|
+
(event) => {
|
|
394
|
+
const t = Date.now();
|
|
395
|
+
if (t - lastMove < CONTINUOUS_THROTTLE_MS) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
lastMove = t;
|
|
399
|
+
recordActivity("mousemove", `${event.clientX}/${event.clientY}`);
|
|
400
|
+
},
|
|
401
|
+
{ passive: true },
|
|
402
|
+
);
|
|
403
|
+
window.addEventListener(
|
|
404
|
+
"scroll",
|
|
405
|
+
() => {
|
|
406
|
+
const t = Date.now();
|
|
407
|
+
if (t - lastScroll < CONTINUOUS_THROTTLE_MS) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
lastScroll = t;
|
|
411
|
+
recordActivity("scroll", `${window.scrollX}/${window.scrollY}`);
|
|
412
|
+
},
|
|
413
|
+
{ passive: true },
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Report outgoing HTTP requests (skipping our own internal traffic).
|
|
417
|
+
const isInternal = (url) => String(url).includes("/.internal/");
|
|
418
|
+
window.fetch = (input, init) => {
|
|
419
|
+
const url =
|
|
420
|
+
typeof input === "string"
|
|
421
|
+
? input
|
|
422
|
+
: input instanceof Request
|
|
423
|
+
? input.url
|
|
424
|
+
: String(input);
|
|
425
|
+
if (!isInternal(url)) {
|
|
426
|
+
const method =
|
|
427
|
+
(init && init.method) ||
|
|
428
|
+
(input instanceof Request ? input.method : "GET");
|
|
429
|
+
recordActivity("request", `${method} ${url}`);
|
|
329
430
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const t = Date.now();
|
|
339
|
-
if (t - lastScroll < CONTINUOUS_THROTTLE_MS) {
|
|
431
|
+
return nativeFetch(input, init);
|
|
432
|
+
};
|
|
433
|
+
// SPA navigations (history API + back/forward).
|
|
434
|
+
const reportNavigation = () =>
|
|
435
|
+
recordActivity("navigation", window.location.href);
|
|
436
|
+
const patchHistory = (method) => {
|
|
437
|
+
const original = window.history[method];
|
|
438
|
+
if (typeof original !== "function") {
|
|
340
439
|
return;
|
|
341
440
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
441
|
+
window.history[method] = (...args) => {
|
|
442
|
+
const result = original.apply(window.history, args);
|
|
443
|
+
reportNavigation();
|
|
444
|
+
return result;
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
patchHistory("pushState");
|
|
448
|
+
patchHistory("replaceState");
|
|
449
|
+
window.addEventListener("popstate", reportNavigation);
|
|
450
|
+
|
|
451
|
+
// Record the page load itself as an activity: after a reload the tab goes
|
|
452
|
+
// hidden (pagehide on the old page) then loads again here, and without this
|
|
453
|
+
// the dashboard would only ever show the "hidden" side of a reload. Sent
|
|
454
|
+
// with the first heartbeat below.
|
|
455
|
+
recordActivity("load", window.location.href);
|
|
456
|
+
}
|
|
457
|
+
|
|
347
458
|
document.addEventListener("visibilitychange", () => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
459
|
+
if (!isLocalClient) {
|
|
460
|
+
// Spell out the direction so the activity reads meaningfully on its own,
|
|
461
|
+
// rather than a bare "visibility" whose value you have to interpret.
|
|
462
|
+
recordActivity(
|
|
463
|
+
document.visibilityState === "visible"
|
|
464
|
+
? "document_becomes_visible"
|
|
465
|
+
: "document_becomes_hidden",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
355
468
|
// push promptly so the dashboard's "active tab" tracks focus changes
|
|
356
469
|
post();
|
|
357
470
|
});
|
|
358
471
|
|
|
359
|
-
// Report outgoing HTTP requests (skipping our own internal traffic).
|
|
360
|
-
const isInternal = (url) => String(url).includes("/.internal/");
|
|
361
|
-
window.fetch = (input, init) => {
|
|
362
|
-
const url =
|
|
363
|
-
typeof input === "string"
|
|
364
|
-
? input
|
|
365
|
-
: input instanceof Request
|
|
366
|
-
? input.url
|
|
367
|
-
: String(input);
|
|
368
|
-
if (!isInternal(url)) {
|
|
369
|
-
const method =
|
|
370
|
-
(init && init.method) ||
|
|
371
|
-
(input instanceof Request ? input.method : "GET");
|
|
372
|
-
recordActivity("request", `${method} ${url}`);
|
|
373
|
-
}
|
|
374
|
-
return nativeFetch(input, init);
|
|
375
|
-
};
|
|
376
|
-
// SPA navigations (history API + back/forward).
|
|
377
|
-
const reportNavigation = () =>
|
|
378
|
-
recordActivity("navigation", window.location.href);
|
|
379
|
-
const patchHistory = (method) => {
|
|
380
|
-
const original = window.history[method];
|
|
381
|
-
if (typeof original !== "function") {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
window.history[method] = (...args) => {
|
|
385
|
-
const result = original.apply(window.history, args);
|
|
386
|
-
reportNavigation();
|
|
387
|
-
return result;
|
|
388
|
-
};
|
|
389
|
-
};
|
|
390
|
-
patchHistory("pushState");
|
|
391
|
-
patchHistory("replaceState");
|
|
392
|
-
window.addEventListener("popstate", reportNavigation);
|
|
393
|
-
|
|
394
|
-
// Record the page load itself as an activity: after a reload the tab goes
|
|
395
|
-
// hidden (pagehide on the old page) then loads again here, and without this the
|
|
396
|
-
// dashboard would only ever show the "hidden" side of a reload. Sent with the
|
|
397
|
-
// first heartbeat below.
|
|
398
|
-
recordActivity("load", window.location.href);
|
|
399
|
-
|
|
400
472
|
// Heartbeat keeps the client "online", refreshes tab info, and lets the server
|
|
401
473
|
// detect a resume.
|
|
402
474
|
post();
|
|
@@ -438,6 +510,9 @@ const setup = () => {
|
|
|
438
510
|
// full reload navigates away before this flushes and surfaces as "load" on
|
|
439
511
|
// the next page; a hot update stays on the page, so this is what records it.
|
|
440
512
|
reload: (event) => {
|
|
513
|
+
if (isLocalClient) {
|
|
514
|
+
return; // the main client records no activity
|
|
515
|
+
}
|
|
441
516
|
const data = event.data || {};
|
|
442
517
|
const reason =
|
|
443
518
|
(typeof data.reason === "string" && data.reason) ||
|
|
@@ -486,7 +561,8 @@ const showClientToast = ({ client, reason }) => {
|
|
|
486
561
|
const link = document.createElement("a");
|
|
487
562
|
link.href = `/.internal/client?id=${encodeURIComponent(client.id)}`;
|
|
488
563
|
link.target = "_blank";
|
|
489
|
-
link.
|
|
564
|
+
link.rel = "noopener";
|
|
565
|
+
link.textContent = "Monitor ↗";
|
|
490
566
|
link.style.cssText = "color:#93c5fd;text-decoration:none;font-weight:600";
|
|
491
567
|
el.appendChild(link);
|
|
492
568
|
const close = document.createElement("button");
|