@jsenv/core 41.4.13 → 41.4.14
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/html/clients_page.html +49 -77
- package/dist/js/client_reporter.js +4 -0
- package/dist/js/page_switcher.js +187 -818
- package/dist/page_picker.js +785 -0
- package/dist/start_dev_server/start_dev_server.js +21 -0
- package/package.json +1 -1
- package/src/plugins/client_monitoring/client/client_reporter.js +4 -0
- package/src/plugins/client_monitoring/client/clients_page.html +48 -74
- package/src/plugins/client_monitoring/jsenv_plugin_client_monitoring.js +18 -0
- package/src/plugins/page_switcher/client/page_switcher.js +193 -819
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +3 -0
- package/src/plugins/protocol_file/client/page_picker.js +783 -0
|
@@ -906,6 +906,13 @@ const osFromUserAgent = (userAgent) => {
|
|
|
906
906
|
return { name: "unknown", version: "" };
|
|
907
907
|
};
|
|
908
908
|
|
|
909
|
+
// A browser driven by a program rather than by someone looking at it: a test
|
|
910
|
+
// run, a screenshot script. Chromium says so in its user-agent ("HeadlessChrome"
|
|
911
|
+
// / "Headless"), the others do not — so the reporter also sends
|
|
912
|
+
// navigator.webdriver, which every automated browser sets, and either one is
|
|
913
|
+
// enough to classify the client (see the headless flag on the record).
|
|
914
|
+
const isHeadlessUserAgent = (userAgent) => /Headless/i.test(userAgent);
|
|
915
|
+
|
|
909
916
|
// The machine the dev server runs on, talking to itself: the main client. A
|
|
910
917
|
// phone (or any other device) reaching the server over the network reports
|
|
911
918
|
// with the machine's LAN address instead — which is why the ip is kept on
|
|
@@ -939,6 +946,10 @@ const jsenvPluginClientMonitoring = () => {
|
|
|
939
946
|
userAgent,
|
|
940
947
|
runtime: runtimeFromRequest(request),
|
|
941
948
|
os: osFromUserAgent(userAgent),
|
|
949
|
+
// Automated browser (test run, script) rather than someone browsing;
|
|
950
|
+
// set from the user-agent here, and again from navigator.webdriver when
|
|
951
|
+
// a report brings it (the only signal for headless firefox/webkit).
|
|
952
|
+
headless: isHeadlessUserAgent(userAgent),
|
|
942
953
|
// The address the reports come from; request.ipForwarded when a proxy
|
|
943
954
|
// sits in between, so the client's own address is kept, not the proxy's.
|
|
944
955
|
ip: request.ipForwarded || request.ip,
|
|
@@ -958,6 +969,7 @@ const jsenvPluginClientMonitoring = () => {
|
|
|
958
969
|
client.userAgent = userAgent;
|
|
959
970
|
client.runtime = runtimeFromRequest(request);
|
|
960
971
|
client.os = osFromUserAgent(userAgent);
|
|
972
|
+
client.headless = client.headless || isHeadlessUserAgent(userAgent);
|
|
961
973
|
}
|
|
962
974
|
// A device changes address (wifi drop, DHCP): the record follows it.
|
|
963
975
|
const ip = request.ipForwarded || request.ip;
|
|
@@ -1076,6 +1088,7 @@ const jsenvPluginClientMonitoring = () => {
|
|
|
1076
1088
|
// parsed { name, version } so pages can show a friendly browser/OS
|
|
1077
1089
|
runtime: client.runtime,
|
|
1078
1090
|
os: client.os,
|
|
1091
|
+
headless: client.headless,
|
|
1079
1092
|
ip: client.ip,
|
|
1080
1093
|
// The main client — the machine the dev server runs on, talking to
|
|
1081
1094
|
// itself over localhost.
|
|
@@ -1133,6 +1146,11 @@ const jsenvPluginClientMonitoring = () => {
|
|
|
1133
1146
|
const wasOnline = !firstEver && isOnline(client);
|
|
1134
1147
|
client.everSeen = true;
|
|
1135
1148
|
client.lastSeen = now();
|
|
1149
|
+
// navigator.webdriver, reported once per batch: true for any browser under
|
|
1150
|
+
// automation, including the headless firefox/webkit the user-agent hides.
|
|
1151
|
+
if (body.webdriver === true) {
|
|
1152
|
+
client.headless = true;
|
|
1153
|
+
}
|
|
1136
1154
|
|
|
1137
1155
|
updateTab(client, body.tab);
|
|
1138
1156
|
pruneTabs(client);
|
|
@@ -1391,6 +1409,9 @@ const jsenvPluginPageSwitcher = () => {
|
|
|
1391
1409
|
html: (urlInfo) => {
|
|
1392
1410
|
const htmlAst = parseHtml({ html: urlInfo.content, url: urlInfo.url });
|
|
1393
1411
|
injectJsenvScript(htmlAst, {
|
|
1412
|
+
// A module: the tree it opens is the shared page picker
|
|
1413
|
+
// (protocol_file/client/page_picker.js), which it imports.
|
|
1414
|
+
type: "module",
|
|
1394
1415
|
src: clientFileUrl$1,
|
|
1395
1416
|
pluginName: "jsenv:page_switcher",
|
|
1396
1417
|
});
|
package/package.json
CHANGED
|
@@ -185,6 +185,10 @@ const setup = () => {
|
|
|
185
185
|
pendingActivities = [];
|
|
186
186
|
const payload = JSON.stringify({
|
|
187
187
|
clientId,
|
|
188
|
+
// The server reads browser/OS from the request headers, but a headless
|
|
189
|
+
// firefox/webkit looks exactly like a windowed one there; this flag is
|
|
190
|
+
// what tells them apart (chromium also says "Headless" in its user-agent).
|
|
191
|
+
webdriver: window.navigator.webdriver === true,
|
|
188
192
|
tab: tabInfo({ closing }),
|
|
189
193
|
activities,
|
|
190
194
|
logs,
|
|
@@ -125,21 +125,6 @@
|
|
|
125
125
|
.btn.primary:hover {
|
|
126
126
|
background: #1d4ed8;
|
|
127
127
|
}
|
|
128
|
-
.picker-filter {
|
|
129
|
-
box-sizing: border-box;
|
|
130
|
-
width: 100%;
|
|
131
|
-
margin-bottom: 10px;
|
|
132
|
-
padding: 8px 10px;
|
|
133
|
-
font: inherit;
|
|
134
|
-
border: 1px solid #d1d5db;
|
|
135
|
-
border-radius: 6px;
|
|
136
|
-
}
|
|
137
|
-
.pagepick {
|
|
138
|
-
width: 100%;
|
|
139
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
140
|
-
text-align: left;
|
|
141
|
-
word-break: break-all;
|
|
142
|
-
}
|
|
143
128
|
.monitor {
|
|
144
129
|
margin-left: 4px;
|
|
145
130
|
padding: 2px 4px;
|
|
@@ -227,6 +212,16 @@
|
|
|
227
212
|
<tbody id="other-rows"></tbody>
|
|
228
213
|
</table>
|
|
229
214
|
|
|
215
|
+
<h2>Headless browsers</h2>
|
|
216
|
+
<p class="intro">
|
|
217
|
+
Browsers driven by a program — a test run, a screenshot script — rather
|
|
218
|
+
than by someone looking at them.
|
|
219
|
+
</p>
|
|
220
|
+
<table>
|
|
221
|
+
<thead id="head3"></thead>
|
|
222
|
+
<tbody id="headless-rows"></tbody>
|
|
223
|
+
</table>
|
|
224
|
+
|
|
230
225
|
<dialog id="dlg">
|
|
231
226
|
<div class="dlg-head">
|
|
232
227
|
<strong id="dlg-title"></strong>
|
|
@@ -238,8 +233,11 @@
|
|
|
238
233
|
</dialog>
|
|
239
234
|
|
|
240
235
|
<script type="module">
|
|
236
|
+
import { openPagePicker } from "../../protocol_file/client/page_picker.js";
|
|
237
|
+
|
|
241
238
|
const selfRows = document.getElementById("self-rows");
|
|
242
239
|
const otherRows = document.getElementById("other-rows");
|
|
240
|
+
const headlessRows = document.getElementById("headless-rows");
|
|
243
241
|
// The monitoring script (injected into this page too) stores our own id.
|
|
244
242
|
let selfId = null;
|
|
245
243
|
try {
|
|
@@ -254,6 +252,7 @@
|
|
|
254
252
|
</tr>`;
|
|
255
253
|
document.getElementById("head").innerHTML = headHtml;
|
|
256
254
|
document.getElementById("head2").innerHTML = headHtml;
|
|
255
|
+
document.getElementById("head3").innerHTML = headHtml;
|
|
257
256
|
const COLSPAN = 8;
|
|
258
257
|
|
|
259
258
|
const ago = (ts) => {
|
|
@@ -420,16 +419,26 @@
|
|
|
420
419
|
const render = (clients) => {
|
|
421
420
|
lastClients = clients;
|
|
422
421
|
const self = selfId ? clients.find((d) => d.id === selfId) : null;
|
|
423
|
-
//
|
|
422
|
+
// Ordered by when they first connected, so rows don't reshuffle.
|
|
423
|
+
const byFirstSeen = (a, b) => a.firstSeen - b.firstSeen;
|
|
424
|
+
// A headless browser is a program's, not a person's: it comes and goes
|
|
425
|
+
// with a test run, so it gets its own table instead of pushing the
|
|
426
|
+
// clients someone is actually browsing with out of sight.
|
|
424
427
|
const others = clients
|
|
425
|
-
.filter((d) => d !== self)
|
|
426
|
-
.sort(
|
|
428
|
+
.filter((d) => d !== self && !d.headless)
|
|
429
|
+
.sort(byFirstSeen);
|
|
430
|
+
const headless = clients
|
|
431
|
+
.filter((d) => d !== self && d.headless)
|
|
432
|
+
.sort(byFirstSeen);
|
|
427
433
|
selfRows.innerHTML = self
|
|
428
434
|
? rowHtml(self)
|
|
429
435
|
: `<tr><td class="empty" colspan="${COLSPAN}">This browser isn't registered yet — it will appear shortly.</td></tr>`;
|
|
430
436
|
otherRows.innerHTML = others.length
|
|
431
437
|
? others.map(rowHtml).join("")
|
|
432
438
|
: `<tr><td class="empty" colspan="${COLSPAN}">No other client has connected.</td></tr>`;
|
|
439
|
+
headlessRows.innerHTML = headless.length
|
|
440
|
+
? headless.map(rowHtml).join("")
|
|
441
|
+
: `<tr><td class="empty" colspan="${COLSPAN}">No headless browser has connected.</td></tr>`;
|
|
433
442
|
};
|
|
434
443
|
|
|
435
444
|
// Dialogs (all tabs / recent activity / user agent).
|
|
@@ -514,68 +523,33 @@
|
|
|
514
523
|
// dev server gone — ignore
|
|
515
524
|
}
|
|
516
525
|
};
|
|
517
|
-
// The .html pages available to navigate to (fetched once, lazily).
|
|
518
|
-
let navigablePages = null;
|
|
519
|
-
const loadPages = async () => {
|
|
520
|
-
if (navigablePages) {
|
|
521
|
-
return navigablePages;
|
|
522
|
-
}
|
|
523
|
-
try {
|
|
524
|
-
navigablePages = await (await fetch("/.internal/pages.json")).json();
|
|
525
|
-
} catch {
|
|
526
|
-
navigablePages = [];
|
|
527
|
-
}
|
|
528
|
-
return navigablePages;
|
|
529
|
-
};
|
|
530
526
|
const shortLabel = (d) => {
|
|
531
527
|
const parts = [d.os && d.os.name, d.runtime && d.runtime.name].filter(
|
|
532
528
|
Boolean,
|
|
533
529
|
);
|
|
534
530
|
return parts.length ? parts.join(" ") : d.id.slice(0, 8);
|
|
535
531
|
};
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
(p) =>
|
|
558
|
-
`<li><button class="btn pagepick" data-url="${escapeHtml(p)}">${escapeHtml(p)}</button></li>`,
|
|
559
|
-
)
|
|
560
|
-
.join("")
|
|
561
|
-
: '<li class="muted">No page matches.</li>';
|
|
562
|
-
};
|
|
563
|
-
renderPages("");
|
|
564
|
-
filterEl.oninput = () => renderPages(filterEl.value);
|
|
565
|
-
filterEl.focus();
|
|
566
|
-
listEl.onclick = (event) => {
|
|
567
|
-
const button = event.target.closest("button.pagepick");
|
|
568
|
-
if (!button) {
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
sendCommand({
|
|
572
|
-
clientId: client.id,
|
|
573
|
-
tabId: client.activeTab ? client.activeTab.id : null,
|
|
574
|
-
type: "navigate",
|
|
575
|
-
url: button.getAttribute("data-url"),
|
|
576
|
-
});
|
|
577
|
-
dlg.close();
|
|
578
|
-
};
|
|
532
|
+
// Which page to send a client to is the same question cmd+K asks on any
|
|
533
|
+
// dev-served page, so it is the same tree that answers it (see
|
|
534
|
+
// protocol_file/client/page_picker.js) — filters, folding and all. Only
|
|
535
|
+
// the ending differs: the url goes to the client's active tab (tabId from
|
|
536
|
+
// activeTab, so no other tab moves) instead of to this window. No href on
|
|
537
|
+
// the rows for that very reason: a click here must not take THIS browser
|
|
538
|
+
// to a page meant for another one.
|
|
539
|
+
const openNavigateDialog = (client) => {
|
|
540
|
+
openPagePicker({
|
|
541
|
+
title: `Navigate ${shortLabel(client)} to…`,
|
|
542
|
+
placeholder: "Go to page…",
|
|
543
|
+
hereUrl: client.activeTab ? client.activeTab.url : null,
|
|
544
|
+
onPick: (page) => {
|
|
545
|
+
sendCommand({
|
|
546
|
+
clientId: client.id,
|
|
547
|
+
tabId: client.activeTab ? client.activeTab.id : null,
|
|
548
|
+
type: "navigate",
|
|
549
|
+
url: page.url,
|
|
550
|
+
});
|
|
551
|
+
},
|
|
552
|
+
});
|
|
579
553
|
};
|
|
580
554
|
|
|
581
555
|
document.addEventListener("click", async (event) => {
|
|
@@ -140,6 +140,13 @@ const osFromUserAgent = (userAgent) => {
|
|
|
140
140
|
return { name: "unknown", version: "" };
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// A browser driven by a program rather than by someone looking at it: a test
|
|
144
|
+
// run, a screenshot script. Chromium says so in its user-agent ("HeadlessChrome"
|
|
145
|
+
// / "Headless"), the others do not — so the reporter also sends
|
|
146
|
+
// navigator.webdriver, which every automated browser sets, and either one is
|
|
147
|
+
// enough to classify the client (see the headless flag on the record).
|
|
148
|
+
const isHeadlessUserAgent = (userAgent) => /Headless/i.test(userAgent);
|
|
149
|
+
|
|
143
150
|
// The machine the dev server runs on, talking to itself: the main client. A
|
|
144
151
|
// phone (or any other device) reaching the server over the network reports
|
|
145
152
|
// with the machine's LAN address instead — which is why the ip is kept on
|
|
@@ -173,6 +180,10 @@ export const jsenvPluginClientMonitoring = () => {
|
|
|
173
180
|
userAgent,
|
|
174
181
|
runtime: runtimeFromRequest(request),
|
|
175
182
|
os: osFromUserAgent(userAgent),
|
|
183
|
+
// Automated browser (test run, script) rather than someone browsing;
|
|
184
|
+
// set from the user-agent here, and again from navigator.webdriver when
|
|
185
|
+
// a report brings it (the only signal for headless firefox/webkit).
|
|
186
|
+
headless: isHeadlessUserAgent(userAgent),
|
|
176
187
|
// The address the reports come from; request.ipForwarded when a proxy
|
|
177
188
|
// sits in between, so the client's own address is kept, not the proxy's.
|
|
178
189
|
ip: request.ipForwarded || request.ip,
|
|
@@ -192,6 +203,7 @@ export const jsenvPluginClientMonitoring = () => {
|
|
|
192
203
|
client.userAgent = userAgent;
|
|
193
204
|
client.runtime = runtimeFromRequest(request);
|
|
194
205
|
client.os = osFromUserAgent(userAgent);
|
|
206
|
+
client.headless = client.headless || isHeadlessUserAgent(userAgent);
|
|
195
207
|
}
|
|
196
208
|
// A device changes address (wifi drop, DHCP): the record follows it.
|
|
197
209
|
const ip = request.ipForwarded || request.ip;
|
|
@@ -310,6 +322,7 @@ export const jsenvPluginClientMonitoring = () => {
|
|
|
310
322
|
// parsed { name, version } so pages can show a friendly browser/OS
|
|
311
323
|
runtime: client.runtime,
|
|
312
324
|
os: client.os,
|
|
325
|
+
headless: client.headless,
|
|
313
326
|
ip: client.ip,
|
|
314
327
|
// The main client — the machine the dev server runs on, talking to
|
|
315
328
|
// itself over localhost.
|
|
@@ -367,6 +380,11 @@ export const jsenvPluginClientMonitoring = () => {
|
|
|
367
380
|
const wasOnline = !firstEver && isOnline(client);
|
|
368
381
|
client.everSeen = true;
|
|
369
382
|
client.lastSeen = now();
|
|
383
|
+
// navigator.webdriver, reported once per batch: true for any browser under
|
|
384
|
+
// automation, including the headless firefox/webkit the user-agent hides.
|
|
385
|
+
if (body.webdriver === true) {
|
|
386
|
+
client.headless = true;
|
|
387
|
+
}
|
|
370
388
|
|
|
371
389
|
updateTab(client, body.tab);
|
|
372
390
|
pruneTabs(client);
|