@everydaydevopsio/pilot 0.5.2 → 0.6.0
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/CHANGELOG.md +64 -0
- package/CLAUDE.md +7 -7
- package/README.md +108 -53
- package/dist/browser/browser-manager.d.ts +60 -0
- package/dist/browser/browser-manager.d.ts.map +1 -0
- package/dist/browser/browser-manager.js +348 -0
- package/dist/browser/browser-manager.js.map +1 -0
- package/dist/browser/chrome-launcher.d.ts +30 -0
- package/dist/browser/chrome-launcher.d.ts.map +1 -0
- package/dist/browser/chrome-launcher.js +217 -0
- package/dist/browser/chrome-launcher.js.map +1 -0
- package/dist/browser/connection.d.ts +23 -0
- package/dist/browser/connection.d.ts.map +1 -0
- package/dist/browser/connection.js +53 -0
- package/dist/browser/connection.js.map +1 -0
- package/dist/browser/events.d.ts +9 -0
- package/dist/browser/events.d.ts.map +1 -0
- package/dist/browser/events.js +99 -0
- package/dist/browser/events.js.map +1 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/inspect/element-ref.d.ts +23 -0
- package/dist/browser/inspect/element-ref.d.ts.map +1 -0
- package/dist/browser/inspect/element-ref.js +52 -0
- package/dist/browser/inspect/element-ref.js.map +1 -0
- package/dist/browser/inspect/snapshot.d.ts +11 -0
- package/dist/browser/inspect/snapshot.d.ts.map +1 -0
- package/dist/browser/inspect/snapshot.js +282 -0
- package/dist/browser/inspect/snapshot.js.map +1 -0
- package/dist/browser/inspect/styles.d.ts +45 -0
- package/dist/browser/inspect/styles.d.ts.map +1 -0
- package/dist/browser/inspect/styles.js +190 -0
- package/dist/browser/inspect/styles.js.map +1 -0
- package/dist/browser/inspect/types.d.ts +35 -0
- package/dist/browser/inspect/types.d.ts.map +1 -0
- package/dist/browser/inspect/types.js +2 -0
- package/dist/browser/inspect/types.js.map +1 -0
- package/dist/browser/interaction/dialogs.d.ts +18 -0
- package/dist/browser/interaction/dialogs.d.ts.map +1 -0
- package/dist/browser/interaction/dialogs.js +35 -0
- package/dist/browser/interaction/dialogs.js.map +1 -0
- package/dist/browser/interaction/downloads.d.ts +23 -0
- package/dist/browser/interaction/downloads.d.ts.map +1 -0
- package/dist/browser/interaction/downloads.js +87 -0
- package/dist/browser/interaction/downloads.js.map +1 -0
- package/dist/browser/interaction/events.d.ts +17 -0
- package/dist/browser/interaction/events.d.ts.map +1 -0
- package/dist/browser/interaction/events.js +66 -0
- package/dist/browser/interaction/events.js.map +1 -0
- package/dist/browser/interaction/ref-resolver.d.ts +18 -0
- package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
- package/dist/browser/interaction/ref-resolver.js +32 -0
- package/dist/browser/interaction/ref-resolver.js.map +1 -0
- package/dist/browser/interaction/uploads.d.ts +4 -0
- package/dist/browser/interaction/uploads.d.ts.map +1 -0
- package/dist/browser/interaction/uploads.js +43 -0
- package/dist/browser/interaction/uploads.js.map +1 -0
- package/dist/browser/network/network-buffer.d.ts +19 -0
- package/dist/browser/network/network-buffer.d.ts.map +1 -0
- package/dist/browser/network/network-buffer.js +118 -0
- package/dist/browser/network/network-buffer.js.map +1 -0
- package/dist/browser/network/network-monitor.d.ts +8 -0
- package/dist/browser/network/network-monitor.d.ts.map +1 -0
- package/dist/browser/network/network-monitor.js +85 -0
- package/dist/browser/network/network-monitor.js.map +1 -0
- package/dist/browser/network/redaction.d.ts +3 -0
- package/dist/browser/network/redaction.d.ts.map +1 -0
- package/dist/browser/network/redaction.js +36 -0
- package/dist/browser/network/redaction.js.map +1 -0
- package/dist/browser/network/types.d.ts +45 -0
- package/dist/browser/network/types.d.ts.map +1 -0
- package/dist/browser/network/types.js +2 -0
- package/dist/browser/network/types.js.map +1 -0
- package/dist/browser/performance/analyze.d.ts +5 -0
- package/dist/browser/performance/analyze.d.ts.map +1 -0
- package/dist/browser/performance/analyze.js +230 -0
- package/dist/browser/performance/analyze.js.map +1 -0
- package/dist/browser/performance/trace.d.ts +10 -0
- package/dist/browser/performance/trace.d.ts.map +1 -0
- package/dist/browser/performance/trace.js +38 -0
- package/dist/browser/performance/trace.js.map +1 -0
- package/dist/browser/performance/types.d.ts +31 -0
- package/dist/browser/performance/types.d.ts.map +1 -0
- package/dist/browser/performance/types.js +2 -0
- package/dist/browser/performance/types.js.map +1 -0
- package/dist/browser/security/origins.d.ts +18 -0
- package/dist/browser/security/origins.d.ts.map +1 -0
- package/dist/browser/security/origins.js +84 -0
- package/dist/browser/security/origins.js.map +1 -0
- package/dist/browser/tabs.d.ts +15 -0
- package/dist/browser/tabs.d.ts.map +1 -0
- package/dist/browser/tabs.js +71 -0
- package/dist/browser/tabs.js.map +1 -0
- package/dist/browser/types.d.ts +31 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser.d.ts +2 -99
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -653
- package/dist/browser.js.map +1 -1
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +38 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/click.d.ts +3 -1
- package/dist/commands/click.d.ts.map +1 -1
- package/dist/commands/click.js +16 -4
- package/dist/commands/click.js.map +1 -1
- package/dist/commands/connect.d.ts +14 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +19 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/dialog.d.ts +19 -0
- package/dist/commands/dialog.d.ts.map +1 -0
- package/dist/commands/dialog.js +51 -0
- package/dist/commands/dialog.js.map +1 -0
- package/dist/commands/downloads.d.ts +30 -0
- package/dist/commands/downloads.d.ts.map +1 -0
- package/dist/commands/downloads.js +66 -0
- package/dist/commands/downloads.js.map +1 -0
- package/dist/commands/fill.d.ts +16 -0
- package/dist/commands/fill.d.ts.map +1 -0
- package/dist/commands/fill.js +15 -0
- package/dist/commands/fill.js.map +1 -0
- package/dist/commands/find.d.ts +14 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +26 -0
- package/dist/commands/find.js.map +1 -0
- package/dist/commands/hover.d.ts +13 -0
- package/dist/commands/hover.d.ts.map +1 -0
- package/dist/commands/hover.js +24 -0
- package/dist/commands/hover.js.map +1 -0
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +13 -0
- package/dist/commands/navigate.js.map +1 -1
- package/dist/commands/network.d.ts +33 -0
- package/dist/commands/network.d.ts.map +1 -0
- package/dist/commands/network.js +84 -0
- package/dist/commands/network.js.map +1 -0
- package/dist/commands/performance.d.ts +12 -0
- package/dist/commands/performance.d.ts.map +1 -0
- package/dist/commands/performance.js +26 -0
- package/dist/commands/performance.js.map +1 -0
- package/dist/commands/press_key.d.ts +10 -0
- package/dist/commands/press_key.d.ts.map +1 -0
- package/dist/commands/press_key.js +103 -0
- package/dist/commands/press_key.js.map +1 -0
- package/dist/commands/scroll.d.ts +21 -0
- package/dist/commands/scroll.d.ts.map +1 -0
- package/dist/commands/scroll.js +54 -0
- package/dist/commands/scroll.js.map +1 -0
- package/dist/commands/select.d.ts +18 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/select.js +53 -0
- package/dist/commands/select.js.map +1 -0
- package/dist/commands/snapshot.d.ts +9 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +7 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/commands/styles.d.ts +13 -0
- package/dist/commands/styles.d.ts.map +1 -0
- package/dist/commands/styles.js +7 -0
- package/dist/commands/styles.js.map +1 -0
- package/dist/commands/type.d.ts +3 -1
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +11 -2
- package/dist/commands/type.js.map +1 -1
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +13 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/mcp/console-buffer.d.ts +9 -0
- package/dist/mcp/console-buffer.d.ts.map +1 -1
- package/dist/mcp/console-buffer.js.map +1 -1
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/browser.d.ts.map +1 -1
- package/dist/mcp/tools/browser.js +99 -8
- package/dist/mcp/tools/browser.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts +5 -0
- package/dist/mcp/tools/connect.d.ts.map +1 -0
- package/dist/mcp/tools/connect.js +48 -0
- package/dist/mcp/tools/connect.js.map +1 -0
- package/dist/mcp/tools/files.d.ts +4 -0
- package/dist/mcp/tools/files.d.ts.map +1 -0
- package/dist/mcp/tools/files.js +85 -0
- package/dist/mcp/tools/files.js.map +1 -0
- package/dist/mcp/tools/interaction.d.ts +4 -0
- package/dist/mcp/tools/interaction.d.ts.map +1 -0
- package/dist/mcp/tools/interaction.js +176 -0
- package/dist/mcp/tools/interaction.js.map +1 -0
- package/dist/mcp/tools/names.d.ts +1 -1
- package/dist/mcp/tools/names.d.ts.map +1 -1
- package/dist/mcp/tools/names.js +19 -1
- package/dist/mcp/tools/names.js.map +1 -1
- package/dist/mcp/tools/network.d.ts +4 -0
- package/dist/mcp/tools/network.d.ts.map +1 -0
- package/dist/mcp/tools/network.js +68 -0
- package/dist/mcp/tools/network.js.map +1 -0
- package/dist/mcp/tools/performance.d.ts +4 -0
- package/dist/mcp/tools/performance.d.ts.map +1 -0
- package/dist/mcp/tools/performance.js +26 -0
- package/dist/mcp/tools/performance.js.map +1 -0
- package/dist/mcp/tools/runtime.d.ts +4 -0
- package/dist/mcp/tools/runtime.d.ts.map +1 -0
- package/dist/mcp/tools/runtime.js +128 -0
- package/dist/mcp/tools/runtime.js.map +1 -0
- package/dist/mcp/tools/snapshot.d.ts +4 -0
- package/dist/mcp/tools/snapshot.d.ts.map +1 -0
- package/dist/mcp/tools/snapshot.js +42 -0
- package/dist/mcp/tools/snapshot.js.map +1 -0
- package/dist/mcp/tools/styles.d.ts +4 -0
- package/dist/mcp/tools/styles.d.ts.map +1 -0
- package/dist/mcp/tools/styles.js +27 -0
- package/dist/mcp/tools/styles.js.map +1 -0
- package/dist/util/config.d.ts.map +1 -1
- package/dist/util/config.js +3 -2
- package/dist/util/config.js.map +1 -1
- package/package.json +3 -3
- package/src/browser/browser-manager.ts +457 -0
- package/src/browser/chrome-launcher.ts +271 -0
- package/src/browser/connection.ts +84 -0
- package/src/browser/events.ts +121 -0
- package/src/browser/index.ts +20 -0
- package/src/browser/inspect/element-ref.ts +68 -0
- package/src/browser/inspect/snapshot.ts +348 -0
- package/src/browser/inspect/styles.ts +258 -0
- package/src/browser/inspect/types.ts +37 -0
- package/src/browser/interaction/dialogs.ts +54 -0
- package/src/browser/interaction/downloads.ts +116 -0
- package/src/browser/interaction/events.ts +79 -0
- package/src/browser/interaction/ref-resolver.ts +53 -0
- package/src/browser/interaction/uploads.ts +60 -0
- package/src/browser/network/network-buffer.ts +133 -0
- package/src/browser/network/network-monitor.ts +107 -0
- package/src/browser/network/redaction.ts +42 -0
- package/src/browser/network/types.ts +47 -0
- package/src/browser/performance/analyze.ts +298 -0
- package/src/browser/performance/trace.ts +60 -0
- package/src/browser/performance/types.ts +34 -0
- package/src/browser/security/origins.ts +102 -0
- package/src/browser/tabs.ts +110 -0
- package/src/browser/types.ts +31 -0
- package/src/browser.ts +22 -798
- package/src/commands/check.ts +59 -0
- package/src/commands/click.ts +18 -4
- package/src/commands/connect.ts +40 -0
- package/src/commands/dialog.ts +79 -0
- package/src/commands/downloads.ts +111 -0
- package/src/commands/fill.ts +34 -0
- package/src/commands/find.ts +52 -0
- package/src/commands/hover.ts +42 -0
- package/src/commands/navigate.ts +20 -0
- package/src/commands/network.ts +141 -0
- package/src/commands/performance.ts +51 -0
- package/src/commands/press_key.ts +134 -0
- package/src/commands/scroll.ts +79 -0
- package/src/commands/select.ts +78 -0
- package/src/commands/snapshot.ts +18 -0
- package/src/commands/styles.ts +24 -0
- package/src/commands/type.ts +12 -2
- package/src/commands/upload.ts +36 -0
- package/src/mcp/console-buffer.ts +10 -0
- package/src/mcp/server.ts +74 -2
- package/src/mcp/tools/browser.ts +113 -7
- package/src/mcp/tools/connect.ts +62 -0
- package/src/mcp/tools/files.ts +120 -0
- package/src/mcp/tools/interaction.ts +232 -0
- package/src/mcp/tools/names.ts +19 -1
- package/src/mcp/tools/network.ts +86 -0
- package/src/mcp/tools/performance.ts +40 -0
- package/src/mcp/tools/runtime.ts +164 -0
- package/src/mcp/tools/snapshot.ts +64 -0
- package/src/mcp/tools/styles.ts +41 -0
- package/src/util/config.ts +3 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { Client } from 'chrome-remote-interface';
|
|
2
|
+
import type {
|
|
3
|
+
PerformanceSummary,
|
|
4
|
+
NavigationTiming,
|
|
5
|
+
LongTask,
|
|
6
|
+
SlowRequest,
|
|
7
|
+
LargeResource
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const LONG_TASK_THRESHOLD_MS = 50;
|
|
11
|
+
const SLOW_REQUEST_THRESHOLD_MS = 1000;
|
|
12
|
+
const LARGE_RESOURCE_THRESHOLD_BYTES = 500 * 1024;
|
|
13
|
+
const MAX_ENTRIES = 100;
|
|
14
|
+
|
|
15
|
+
interface TraceEvent {
|
|
16
|
+
cat?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
ph?: string;
|
|
19
|
+
dur?: number;
|
|
20
|
+
ts?: number;
|
|
21
|
+
args?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findNavigationStart(events: TraceEvent[]): number {
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event.name === 'navigationStart' && event.ts) {
|
|
27
|
+
return event.ts;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Fallback: earliest timestamp in the trace
|
|
31
|
+
let earliest = Infinity;
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
if (event.ts && event.ts < earliest) {
|
|
34
|
+
earliest = event.ts;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return earliest === Infinity ? 0 : earliest;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractNavigationTiming(events: TraceEvent[]): NavigationTiming {
|
|
41
|
+
const timing: NavigationTiming = {};
|
|
42
|
+
const baseTs = findNavigationStart(events);
|
|
43
|
+
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
if (event.name === 'domContentLoadedEventEnd' && event.ts !== undefined) {
|
|
46
|
+
timing.domContentLoaded = (event.ts - baseTs) / 1000;
|
|
47
|
+
}
|
|
48
|
+
if (event.name === 'loadEventEnd' && event.ts !== undefined) {
|
|
49
|
+
timing.load = (event.ts - baseTs) / 1000;
|
|
50
|
+
}
|
|
51
|
+
if (event.name === 'firstPaint' && event.ts !== undefined) {
|
|
52
|
+
timing.firstPaint = (event.ts - baseTs) / 1000;
|
|
53
|
+
}
|
|
54
|
+
if (event.name === 'firstContentfulPaint' && event.ts !== undefined) {
|
|
55
|
+
timing.firstContentfulPaint = (event.ts - baseTs) / 1000;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return timing;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractLongTasks(events: TraceEvent[]): LongTask[] {
|
|
63
|
+
const tasks: LongTask[] = [];
|
|
64
|
+
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
if (
|
|
67
|
+
event.name === 'RunTask' &&
|
|
68
|
+
event.ph === 'X' &&
|
|
69
|
+
event.dur &&
|
|
70
|
+
event.dur / 1000 > LONG_TASK_THRESHOLD_MS
|
|
71
|
+
) {
|
|
72
|
+
tasks.push({
|
|
73
|
+
name: event.name,
|
|
74
|
+
duration: Math.round(event.dur / 1000),
|
|
75
|
+
startTime: event.ts ? Math.round(event.ts / 1000) : 0
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return tasks.sort((a, b) => b.duration - a.duration).slice(0, MAX_ENTRIES);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractSlowRequests(events: TraceEvent[]): SlowRequest[] {
|
|
84
|
+
const requests = new Map<
|
|
85
|
+
string,
|
|
86
|
+
{ url: string; start: number; end?: number; method?: string }
|
|
87
|
+
>();
|
|
88
|
+
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (event.name === 'ResourceSendRequest' && event.args) {
|
|
91
|
+
const data = event.args.data as
|
|
92
|
+
| { requestId?: string; url?: string; requestMethod?: string }
|
|
93
|
+
| undefined;
|
|
94
|
+
if (data?.requestId && data.url) {
|
|
95
|
+
requests.set(data.requestId, {
|
|
96
|
+
url: data.url,
|
|
97
|
+
start: event.ts ?? 0,
|
|
98
|
+
method: data.requestMethod
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (event.name === 'ResourceFinish' && event.args) {
|
|
103
|
+
const data = event.args.data as { requestId?: string } | undefined;
|
|
104
|
+
if (data?.requestId) {
|
|
105
|
+
const req = requests.get(data.requestId);
|
|
106
|
+
if (req) {
|
|
107
|
+
req.end = event.ts;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const slow: SlowRequest[] = [];
|
|
114
|
+
for (const req of requests.values()) {
|
|
115
|
+
if (req.end) {
|
|
116
|
+
const duration = Math.round((req.end - req.start) / 1000);
|
|
117
|
+
if (duration > SLOW_REQUEST_THRESHOLD_MS) {
|
|
118
|
+
slow.push({ url: req.url, duration, method: req.method });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return slow.sort((a, b) => b.duration - a.duration).slice(0, MAX_ENTRIES);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractLargeResources(events: TraceEvent[]): LargeResource[] {
|
|
127
|
+
// Build a requestId → URL map from ResourceSendRequest events
|
|
128
|
+
const urlMap = new Map<string, string>();
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
if (event.name === 'ResourceSendRequest' && event.args) {
|
|
131
|
+
const data = event.args.data as
|
|
132
|
+
{ requestId?: string; url?: string } | undefined;
|
|
133
|
+
if (data?.requestId && data.url) {
|
|
134
|
+
urlMap.set(data.requestId, data.url);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resources: LargeResource[] = [];
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
if (event.name === 'ResourceFinish' && event.args) {
|
|
142
|
+
const data = event.args.data as
|
|
143
|
+
{ decodedBodyLength?: number; requestId?: string } | undefined;
|
|
144
|
+
if (
|
|
145
|
+
data?.decodedBodyLength &&
|
|
146
|
+
data.decodedBodyLength > LARGE_RESOURCE_THRESHOLD_BYTES
|
|
147
|
+
) {
|
|
148
|
+
resources.push({
|
|
149
|
+
url: (data.requestId && urlMap.get(data.requestId)) || '',
|
|
150
|
+
size: data.decodedBodyLength
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resources.sort((a, b) => b.size - a.size).slice(0, MAX_ENTRIES);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractJsExecutionTime(events: TraceEvent[]): number {
|
|
160
|
+
let totalUs = 0;
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
if (event.cat?.includes('v8') && event.ph === 'X' && event.dur) {
|
|
163
|
+
totalUs += event.dur;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return Math.round(totalUs / 1000);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function analyzePerformance(
|
|
170
|
+
client: Client,
|
|
171
|
+
traceEvents: unknown[]
|
|
172
|
+
): Promise<PerformanceSummary> {
|
|
173
|
+
// Get current performance metrics from CDP
|
|
174
|
+
const { metrics: rawMetrics } = await client.Performance.getMetrics();
|
|
175
|
+
const metrics: Record<string, number> = {};
|
|
176
|
+
for (const m of rawMetrics) {
|
|
177
|
+
metrics[m.name] = m.value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const events = traceEvents as TraceEvent[];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
navigationTiming: extractNavigationTiming(events),
|
|
184
|
+
metrics,
|
|
185
|
+
longTasks: extractLongTasks(events),
|
|
186
|
+
slowRequests: extractSlowRequests(events),
|
|
187
|
+
largeResources: extractLargeResources(events),
|
|
188
|
+
totalJsExecutionMs: extractJsExecutionTime(events),
|
|
189
|
+
traceEventCount: events.length
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function formatPerformanceSummary(summary: PerformanceSummary): string {
|
|
194
|
+
const lines: string[] = ['Performance Summary:\n'];
|
|
195
|
+
|
|
196
|
+
// Navigation timing — use explicit undefined checks so 0ms values are shown
|
|
197
|
+
const nt = summary.navigationTiming;
|
|
198
|
+
lines.push('Navigation Timing:');
|
|
199
|
+
if (nt.domContentLoaded !== undefined)
|
|
200
|
+
lines.push(` DOMContentLoaded: ${Math.round(nt.domContentLoaded)}ms`);
|
|
201
|
+
if (nt.load !== undefined) lines.push(` Load: ${Math.round(nt.load)}ms`);
|
|
202
|
+
if (nt.firstPaint !== undefined)
|
|
203
|
+
lines.push(` First Paint: ${Math.round(nt.firstPaint)}ms`);
|
|
204
|
+
if (nt.firstContentfulPaint !== undefined)
|
|
205
|
+
lines.push(
|
|
206
|
+
` First Contentful Paint: ${Math.round(nt.firstContentfulPaint)}ms`
|
|
207
|
+
);
|
|
208
|
+
if (
|
|
209
|
+
nt.domContentLoaded === undefined &&
|
|
210
|
+
nt.load === undefined &&
|
|
211
|
+
nt.firstPaint === undefined &&
|
|
212
|
+
nt.firstContentfulPaint === undefined
|
|
213
|
+
) {
|
|
214
|
+
lines.push(' (no timing data captured)');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Key metrics
|
|
218
|
+
const keyMetrics = [
|
|
219
|
+
'JSHeapUsedSize',
|
|
220
|
+
'JSHeapTotalSize',
|
|
221
|
+
'Documents',
|
|
222
|
+
'Frames',
|
|
223
|
+
'LayoutCount',
|
|
224
|
+
'RecalcStyleCount',
|
|
225
|
+
'ScriptDuration',
|
|
226
|
+
'TaskDuration'
|
|
227
|
+
];
|
|
228
|
+
const hasMetrics = keyMetrics.some((k) => summary.metrics[k] !== undefined);
|
|
229
|
+
if (hasMetrics) {
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push('Key Metrics:');
|
|
232
|
+
for (const k of keyMetrics) {
|
|
233
|
+
if (summary.metrics[k] !== undefined) {
|
|
234
|
+
let val = summary.metrics[k];
|
|
235
|
+
let unit = '';
|
|
236
|
+
if (k.includes('Size')) {
|
|
237
|
+
val = Math.round(val / 1024);
|
|
238
|
+
unit = ' KB';
|
|
239
|
+
} else if (k.includes('Duration')) {
|
|
240
|
+
val = Math.round(val * 1000);
|
|
241
|
+
unit = ' ms';
|
|
242
|
+
}
|
|
243
|
+
lines.push(` ${k}: ${val}${unit}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// JS execution time
|
|
249
|
+
if (summary.totalJsExecutionMs) {
|
|
250
|
+
lines.push('');
|
|
251
|
+
lines.push(`Total JS Execution: ${summary.totalJsExecutionMs}ms`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Long tasks
|
|
255
|
+
if (summary.longTasks.length > 0) {
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(
|
|
258
|
+
`Long Tasks (>${LONG_TASK_THRESHOLD_MS}ms): ${summary.longTasks.length}`
|
|
259
|
+
);
|
|
260
|
+
for (const t of summary.longTasks.slice(0, 10)) {
|
|
261
|
+
lines.push(` ${t.name}: ${t.duration}ms`);
|
|
262
|
+
}
|
|
263
|
+
if (summary.longTasks.length > 10) {
|
|
264
|
+
lines.push(` ... and ${summary.longTasks.length - 10} more`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Slow requests
|
|
269
|
+
if (summary.slowRequests.length > 0) {
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push(
|
|
272
|
+
`Slow Requests (>${SLOW_REQUEST_THRESHOLD_MS}ms): ${summary.slowRequests.length}`
|
|
273
|
+
);
|
|
274
|
+
for (const r of summary.slowRequests.slice(0, 10)) {
|
|
275
|
+
lines.push(` ${r.method ?? 'GET'} ${r.url}: ${r.duration}ms`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Large resources
|
|
280
|
+
if (summary.largeResources.length > 0) {
|
|
281
|
+
lines.push('');
|
|
282
|
+
lines.push(
|
|
283
|
+
`Large Resources (>${Math.round(LARGE_RESOURCE_THRESHOLD_BYTES / 1024)}KB): ${summary.largeResources.length}`
|
|
284
|
+
);
|
|
285
|
+
for (const r of summary.largeResources.slice(0, 10)) {
|
|
286
|
+
lines.push(
|
|
287
|
+
` ${Math.round(r.size / 1024)}KB ${r.url || '(unknown url)'}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (summary.traceEventCount) {
|
|
293
|
+
lines.push('');
|
|
294
|
+
lines.push(`Trace events processed: ${summary.traceEventCount}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Client } from 'chrome-remote-interface';
|
|
2
|
+
|
|
3
|
+
const MAX_TRACE_EVENTS = 100000;
|
|
4
|
+
|
|
5
|
+
export interface TraceState {
|
|
6
|
+
tracing: boolean;
|
|
7
|
+
events: unknown[];
|
|
8
|
+
listenerClient: WeakRef<Client> | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createTraceState(): TraceState {
|
|
12
|
+
return { tracing: false, events: [], listenerClient: null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function startTrace(
|
|
16
|
+
client: Client,
|
|
17
|
+
state: TraceState
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
if (state.tracing) {
|
|
20
|
+
throw new Error('Tracing is already active. Stop it first.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
state.events = [];
|
|
24
|
+
state.tracing = true;
|
|
25
|
+
|
|
26
|
+
// Only attach listener if not already attached to this client
|
|
27
|
+
if (state.listenerClient?.deref() !== client) {
|
|
28
|
+
client.Tracing.dataCollected((params) => {
|
|
29
|
+
if (state.events.length < MAX_TRACE_EVENTS) {
|
|
30
|
+
state.events.push(...params.value);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
state.listenerClient = new WeakRef(client);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await client.Tracing.start({
|
|
37
|
+
categories:
|
|
38
|
+
'devtools.timeline,loading,v8,v8.execute,blink.user_timing,navigation',
|
|
39
|
+
transferMode: 'ReportEvents'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function stopTrace(
|
|
44
|
+
client: Client,
|
|
45
|
+
state: TraceState
|
|
46
|
+
): Promise<unknown[]> {
|
|
47
|
+
if (!state.tracing) {
|
|
48
|
+
throw new Error('No active trace. Call start first.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await new Promise<void>((resolve) => {
|
|
52
|
+
client.Tracing.tracingComplete(() => {
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
void client.Tracing.end();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
state.tracing = false;
|
|
59
|
+
return state.events;
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface NavigationTiming {
|
|
2
|
+
domContentLoaded?: number;
|
|
3
|
+
load?: number;
|
|
4
|
+
firstPaint?: number;
|
|
5
|
+
firstContentfulPaint?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LongTask {
|
|
9
|
+
name: string;
|
|
10
|
+
duration: number;
|
|
11
|
+
startTime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SlowRequest {
|
|
15
|
+
url: string;
|
|
16
|
+
duration: number;
|
|
17
|
+
method?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LargeResource {
|
|
21
|
+
url: string;
|
|
22
|
+
size: number;
|
|
23
|
+
type?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PerformanceSummary {
|
|
27
|
+
navigationTiming: NavigationTiming;
|
|
28
|
+
metrics: Record<string, number>;
|
|
29
|
+
longTasks: LongTask[];
|
|
30
|
+
slowRequests: SlowRequest[];
|
|
31
|
+
largeResources: LargeResource[];
|
|
32
|
+
totalJsExecutionMs?: number;
|
|
33
|
+
traceEventCount?: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin allow/block list evaluation.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - Block wins over allow.
|
|
6
|
+
* - Both empty = everything allowed.
|
|
7
|
+
* - Supports wildcard patterns: *.example.com matches sub.example.com
|
|
8
|
+
* - Patterns with a scheme (https://...) match the full origin.
|
|
9
|
+
* - Patterns without a scheme match the host (+port) only, any scheme.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function getOriginList(envVar: string): string[] {
|
|
13
|
+
const env = process.env[envVar];
|
|
14
|
+
if (!env) return [];
|
|
15
|
+
return env
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((p) => p.trim())
|
|
18
|
+
.filter((p) => p.length > 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function patternToRegex(pattern: string): RegExp {
|
|
22
|
+
// Escape regex special chars except *
|
|
23
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
24
|
+
// Replace * with regex wildcard
|
|
25
|
+
const regexStr = escaped.replace(/\*/g, '.*');
|
|
26
|
+
return new RegExp(`^${regexStr}$`, 'i');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasScheme(pattern: string): boolean {
|
|
30
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(pattern);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesPattern(
|
|
34
|
+
origin: string,
|
|
35
|
+
host: string,
|
|
36
|
+
pattern: string
|
|
37
|
+
): boolean {
|
|
38
|
+
if (hasScheme(pattern)) {
|
|
39
|
+
// Pattern includes scheme — match against full origin
|
|
40
|
+
return patternToRegex(pattern).test(origin);
|
|
41
|
+
}
|
|
42
|
+
// Scheme-less pattern — match against host (+port) only
|
|
43
|
+
return patternToRegex(pattern).test(host);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function matchesAny(origin: string, host: string, patterns: string[]): boolean {
|
|
47
|
+
return patterns.some((p) => matchesPattern(origin, host, p));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function extractOrigin(url: string): string {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = new URL(url);
|
|
53
|
+
return parsed.origin;
|
|
54
|
+
} catch {
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function extractHost(url: string): string {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(url);
|
|
62
|
+
return parsed.host;
|
|
63
|
+
} catch {
|
|
64
|
+
return url;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface OriginCheckResult {
|
|
69
|
+
allowed: boolean;
|
|
70
|
+
reason?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function checkOrigin(url: string): OriginCheckResult {
|
|
74
|
+
const blocked = getOriginList('PILOT_BLOCKED_ORIGINS');
|
|
75
|
+
const allowed = getOriginList('PILOT_ALLOWED_ORIGINS');
|
|
76
|
+
|
|
77
|
+
// Both empty = everything allowed
|
|
78
|
+
if (blocked.length === 0 && allowed.length === 0) {
|
|
79
|
+
return { allowed: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const origin = extractOrigin(url);
|
|
83
|
+
const host = extractHost(url);
|
|
84
|
+
|
|
85
|
+
// Block wins over allow
|
|
86
|
+
if (blocked.length > 0 && matchesAny(origin, host, blocked)) {
|
|
87
|
+
return {
|
|
88
|
+
allowed: false,
|
|
89
|
+
reason: `Origin "${origin}" is blocked by PILOT_BLOCKED_ORIGINS`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If allow list exists, origin must match it
|
|
94
|
+
if (allowed.length > 0 && !matchesAny(origin, host, allowed)) {
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
reason: `Origin "${origin}" is not in PILOT_ALLOWED_ORIGINS`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { allowed: true };
|
|
102
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
import type { Client } from 'chrome-remote-interface';
|
|
3
|
+
import { getLogger } from '../util/logger.js';
|
|
4
|
+
import type { TabInfo } from './types.js';
|
|
5
|
+
import type { ConnectionInfo } from './connection.js';
|
|
6
|
+
import { enableDomains } from './connection.js';
|
|
7
|
+
import { attachEventListeners } from './events.js';
|
|
8
|
+
import type { ViewportConfig } from '../viewport.js';
|
|
9
|
+
|
|
10
|
+
export async function listTabs(
|
|
11
|
+
connInfo: ConnectionInfo,
|
|
12
|
+
activeTargetId: string | null
|
|
13
|
+
): Promise<TabInfo[]> {
|
|
14
|
+
const targets = await CDP.List({ host: connInfo.host, port: connInfo.port });
|
|
15
|
+
return targets
|
|
16
|
+
.filter((t) => t.type === 'page')
|
|
17
|
+
.map((t) => ({
|
|
18
|
+
targetId: t.id,
|
|
19
|
+
url: t.url,
|
|
20
|
+
title: t.title,
|
|
21
|
+
active: t.id === activeTargetId
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createTab(
|
|
26
|
+
connInfo: ConnectionInfo,
|
|
27
|
+
url?: string
|
|
28
|
+
): Promise<TabInfo> {
|
|
29
|
+
const encodedUrl = url ? encodeURI(url) : undefined;
|
|
30
|
+
const target = await CDP.New({
|
|
31
|
+
host: connInfo.host,
|
|
32
|
+
port: connInfo.port,
|
|
33
|
+
url: encodedUrl
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
targetId: target.id,
|
|
37
|
+
url: target.url,
|
|
38
|
+
title: target.title,
|
|
39
|
+
active: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function closeTab(
|
|
44
|
+
connInfo: ConnectionInfo,
|
|
45
|
+
targetId: string,
|
|
46
|
+
activeTargetId: string | null
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
if (targetId === activeTargetId) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Cannot close the active tab. Switch to another tab first.'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
await CDP.Close({ host: connInfo.host, port: connInfo.port, id: targetId });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SwitchTabResult {
|
|
57
|
+
client: Client;
|
|
58
|
+
targetId: string;
|
|
59
|
+
url: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function switchTab(
|
|
63
|
+
connInfo: ConnectionInfo,
|
|
64
|
+
targetId: string,
|
|
65
|
+
currentTargetId: string | null,
|
|
66
|
+
currentClient: Client | null,
|
|
67
|
+
viewportConfig: ViewportConfig | null,
|
|
68
|
+
eventListenerOpts: Parameters<typeof attachEventListeners>[1]
|
|
69
|
+
): Promise<SwitchTabResult> {
|
|
70
|
+
if (targetId === currentTargetId && currentClient) {
|
|
71
|
+
return {
|
|
72
|
+
client: currentClient,
|
|
73
|
+
targetId,
|
|
74
|
+
url: ''
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Verify the target exists
|
|
79
|
+
const targets = await CDP.List({
|
|
80
|
+
host: connInfo.host,
|
|
81
|
+
port: connInfo.port
|
|
82
|
+
});
|
|
83
|
+
const target = targets.find((t) => t.id === targetId && t.type === 'page');
|
|
84
|
+
if (!target) {
|
|
85
|
+
throw new Error(`No page tab found with targetId: ${targetId}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Close old client connection (but not the tab)
|
|
89
|
+
if (currentClient) {
|
|
90
|
+
await currentClient.close();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Connect to the new target
|
|
94
|
+
const client = await CDP({
|
|
95
|
+
host: connInfo.host,
|
|
96
|
+
port: connInfo.port,
|
|
97
|
+
target: targetId
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await enableDomains(client, viewportConfig);
|
|
101
|
+
attachEventListeners(client, eventListenerOpts);
|
|
102
|
+
|
|
103
|
+
// Activate the tab in the browser UI
|
|
104
|
+
await client.Target.activateTarget({ targetId });
|
|
105
|
+
|
|
106
|
+
const logger = getLogger();
|
|
107
|
+
logger.info({ targetId, url: target.url }, 'Switched to tab');
|
|
108
|
+
|
|
109
|
+
return { client, targetId, url: target.url };
|
|
110
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Client } from 'chrome-remote-interface';
|
|
2
|
+
|
|
3
|
+
export type BrowserEventCallback = (event: string, data: unknown) => void;
|
|
4
|
+
|
|
5
|
+
export interface TabInfo {
|
|
6
|
+
targetId: string;
|
|
7
|
+
url: string;
|
|
8
|
+
title: string;
|
|
9
|
+
active: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BrowserState {
|
|
13
|
+
client: Client | null;
|
|
14
|
+
targetId: string | null;
|
|
15
|
+
url: string | null;
|
|
16
|
+
connected: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LaunchOptions {
|
|
20
|
+
headless?: boolean;
|
|
21
|
+
chromePath?: string;
|
|
22
|
+
profileName?: string;
|
|
23
|
+
viewport?: string;
|
|
24
|
+
viewportWidth?: number;
|
|
25
|
+
viewportHeight?: number;
|
|
26
|
+
deviceScaleFactor?: number;
|
|
27
|
+
responsive?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SandboxDecision =
|
|
31
|
+
{ disable: false } | { disable: true; reason: 'env_override' | 'root_user' };
|