chrome-devtools-mcp 0.14.0 → 0.15.1
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/README.md +21 -0
- package/build/src/McpContext.js +16 -2
- package/build/src/McpResponse.js +6 -0
- package/build/src/WaitForHelper.js +2 -2
- package/build/src/cli.js +20 -3
- package/build/src/main.js +9 -1
- package/build/src/telemetry/clearcut-logger.js +3 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +174 -21
- package/build/src/telemetry/watchdog/main.js +39 -10
- package/build/src/telemetry/watchdog-client.js +9 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +1418 -0
- package/build/src/third_party/bundled-packages.json +8 -0
- package/build/src/third_party/index.js +106 -79
- package/build/src/tools/emulation.js +21 -0
- package/build/src/tools/pages.js +18 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -27,6 +27,22 @@ allowing them to inspect, debug, and modify any data in the browser or DevTools.
|
|
|
27
27
|
Avoid sharing sensitive or personal information that you don't want to share with
|
|
28
28
|
MCP clients.
|
|
29
29
|
|
|
30
|
+
## **Usage statistics**
|
|
31
|
+
|
|
32
|
+
Google collects usage statistics (such as tool invocation success rates, latency, and environment information) to improve the reliability and performance of Chrome DevTools MCP.
|
|
33
|
+
|
|
34
|
+
Data collection is **enabled by default**. You can opt-out by passing the `--no-usage-statistics` flag when starting the server:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"args": ["-y", "chrome-devtools-mcp@latest", "--no-usage-statistics"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Google handles this data in accordance with the [Google Privacy Policy](https://policies.google.com/privacy).
|
|
41
|
+
|
|
42
|
+
Google's collection of usage statistics for Chrome DevTools MCP is independent from the Chrome browser's usage statistics. Opting out of Chrome metrics does not automatically opt you out of this tool, and vice-versa.
|
|
43
|
+
|
|
44
|
+
Collection is disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
|
|
45
|
+
|
|
30
46
|
## Requirements
|
|
31
47
|
|
|
32
48
|
- [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
|
|
@@ -450,6 +466,11 @@ The Chrome DevTools MCP server supports the following configuration option:
|
|
|
450
466
|
- **Type:** boolean
|
|
451
467
|
- **Default:** `true`
|
|
452
468
|
|
|
469
|
+
- **`--usageStatistics`/ `--usage-statistics`**
|
|
470
|
+
Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
|
|
471
|
+
- **Type:** boolean
|
|
472
|
+
- **Default:** `true`
|
|
473
|
+
|
|
453
474
|
<!-- END AUTO GENERATED OPTIONS -->
|
|
454
475
|
|
|
455
476
|
Pass them via the `args` property in the JSON configuration. For example:
|
package/build/src/McpContext.js
CHANGED
|
@@ -60,6 +60,7 @@ export class McpContext {
|
|
|
60
60
|
#geolocationMap = new WeakMap();
|
|
61
61
|
#viewportMap = new WeakMap();
|
|
62
62
|
#userAgentMap = new WeakMap();
|
|
63
|
+
#colorSchemeMap = new WeakMap();
|
|
63
64
|
#dialog;
|
|
64
65
|
#pageIdMap = new WeakMap();
|
|
65
66
|
#nextPageId = 1;
|
|
@@ -249,6 +250,19 @@ export class McpContext {
|
|
|
249
250
|
const page = this.getSelectedPage();
|
|
250
251
|
return this.#userAgentMap.get(page) ?? null;
|
|
251
252
|
}
|
|
253
|
+
setColorScheme(scheme) {
|
|
254
|
+
const page = this.getSelectedPage();
|
|
255
|
+
if (scheme === null) {
|
|
256
|
+
this.#colorSchemeMap.delete(page);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.#colorSchemeMap.set(page, scheme);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
getColorScheme() {
|
|
263
|
+
const page = this.getSelectedPage();
|
|
264
|
+
return this.#colorSchemeMap.get(page) ?? null;
|
|
265
|
+
}
|
|
252
266
|
setIsRunningPerformanceTrace(x) {
|
|
253
267
|
this.#isRunningTrace = x;
|
|
254
268
|
}
|
|
@@ -537,12 +551,12 @@ export class McpContext {
|
|
|
537
551
|
getWaitForHelper(page, cpuMultiplier, networkMultiplier) {
|
|
538
552
|
return new WaitForHelper(page, cpuMultiplier, networkMultiplier);
|
|
539
553
|
}
|
|
540
|
-
waitForEventsAfterAction(action) {
|
|
554
|
+
waitForEventsAfterAction(action, options) {
|
|
541
555
|
const page = this.getSelectedPage();
|
|
542
556
|
const cpuMultiplier = this.getCpuThrottlingRate();
|
|
543
557
|
const networkMultiplier = getNetworkMultiplierFromString(this.getNetworkConditions());
|
|
544
558
|
const waitForHelper = this.getWaitForHelper(page, cpuMultiplier, networkMultiplier);
|
|
545
|
-
return waitForHelper.waitForEventsAfterAction(action);
|
|
559
|
+
return waitForHelper.waitForEventsAfterAction(action, options);
|
|
546
560
|
}
|
|
547
561
|
getNetworkRequestStableId(request) {
|
|
548
562
|
return this.#networkCollector.getIdForResource(request);
|
package/build/src/McpResponse.js
CHANGED
|
@@ -308,6 +308,12 @@ export class McpResponse {
|
|
|
308
308
|
response.push(`Emulating: ${cpuThrottlingRate}x slowdown`);
|
|
309
309
|
structuredContent.cpuThrottlingRate = cpuThrottlingRate;
|
|
310
310
|
}
|
|
311
|
+
const colorScheme = context.getColorScheme();
|
|
312
|
+
if (colorScheme) {
|
|
313
|
+
response.push(`## Color Scheme emulation`);
|
|
314
|
+
response.push(`Emulating: ${colorScheme}`);
|
|
315
|
+
structuredContent.colorScheme = colorScheme;
|
|
316
|
+
}
|
|
311
317
|
const dialog = context.getDialog();
|
|
312
318
|
if (dialog) {
|
|
313
319
|
const defaultValueIfNeeded = dialog.type() === 'prompt'
|
|
@@ -103,12 +103,12 @@ export class WaitForHelper {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
-
async waitForEventsAfterAction(action) {
|
|
106
|
+
async waitForEventsAfterAction(action, options) {
|
|
107
107
|
const navigationFinished = this.waitForNavigationStarted()
|
|
108
108
|
.then(navigationStated => {
|
|
109
109
|
if (navigationStated) {
|
|
110
110
|
return this.#page.waitForNavigation({
|
|
111
|
-
timeout: this.#navigationTimeout,
|
|
111
|
+
timeout: options?.timeout ?? this.#navigationTimeout,
|
|
112
112
|
signal: this.#abortController.signal,
|
|
113
113
|
});
|
|
114
114
|
}
|
package/build/src/cli.js
CHANGED
|
@@ -190,10 +190,23 @@ export const cliOptions = {
|
|
|
190
190
|
},
|
|
191
191
|
usageStatistics: {
|
|
192
192
|
type: 'boolean',
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
default: true,
|
|
194
|
+
describe: 'Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.',
|
|
195
|
+
},
|
|
196
|
+
clearcutEndpoint: {
|
|
197
|
+
type: 'string',
|
|
198
|
+
hidden: true,
|
|
199
|
+
describe: 'Endpoint for Clearcut telemetry.',
|
|
200
|
+
},
|
|
201
|
+
clearcutForceFlushIntervalMs: {
|
|
202
|
+
type: 'number',
|
|
195
203
|
hidden: true,
|
|
196
|
-
describe: '
|
|
204
|
+
describe: 'Force flush interval in milliseconds (for testing).',
|
|
205
|
+
},
|
|
206
|
+
clearcutIncludePidHeader: {
|
|
207
|
+
type: 'boolean',
|
|
208
|
+
hidden: true,
|
|
209
|
+
describe: 'Include watchdog PID in Clearcut request headers (for testing).',
|
|
197
210
|
},
|
|
198
211
|
};
|
|
199
212
|
export function parseArguments(version, argv = process.argv) {
|
|
@@ -260,6 +273,10 @@ export function parseArguments(version, argv = process.argv) {
|
|
|
260
273
|
'$0 --auto-connect --channel=canary',
|
|
261
274
|
'Connect to a canary Chrome instance (Chrome 144+) running instead of launching a new instance',
|
|
262
275
|
],
|
|
276
|
+
[
|
|
277
|
+
'$0 --no-usage-statistics',
|
|
278
|
+
'Do not send usage statistics https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics.',
|
|
279
|
+
],
|
|
263
280
|
]);
|
|
264
281
|
return yargsInstance
|
|
265
282
|
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
package/build/src/main.js
CHANGED
|
@@ -20,15 +20,23 @@ import { ToolCategory } from './tools/categories.js';
|
|
|
20
20
|
import { tools } from './tools/tools.js';
|
|
21
21
|
// If moved update release-please config
|
|
22
22
|
// x-release-please-start-version
|
|
23
|
-
const VERSION = '0.
|
|
23
|
+
const VERSION = '0.15.1';
|
|
24
24
|
// x-release-please-end
|
|
25
25
|
export const args = parseArguments(VERSION);
|
|
26
26
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
27
|
+
if (process.env['CI'] ||
|
|
28
|
+
process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS']) {
|
|
29
|
+
console.error("turning off usage statistics. process.env['CI'] || process.env['CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS'] is set.");
|
|
30
|
+
args.usageStatistics = false;
|
|
31
|
+
}
|
|
27
32
|
let clearcutLogger;
|
|
28
33
|
if (args.usageStatistics) {
|
|
29
34
|
clearcutLogger = new ClearcutLogger({
|
|
30
35
|
logFile: args.logFile,
|
|
31
36
|
appVersion: VERSION,
|
|
37
|
+
clearcutEndpoint: args.clearcutEndpoint,
|
|
38
|
+
clearcutForceFlushIntervalMs: args.clearcutForceFlushIntervalMs,
|
|
39
|
+
clearcutIncludePidHeader: args.clearcutIncludePidHeader,
|
|
32
40
|
});
|
|
33
41
|
}
|
|
34
42
|
process.on('unhandledRejection', (reason, promise) => {
|
|
@@ -33,6 +33,9 @@ export class ClearcutLogger {
|
|
|
33
33
|
appVersion: options.appVersion,
|
|
34
34
|
osType: detectOsType(),
|
|
35
35
|
logFile: options.logFile,
|
|
36
|
+
clearcutEndpoint: options.clearcutEndpoint,
|
|
37
|
+
clearcutForceFlushIntervalMs: options.clearcutForceFlushIntervalMs,
|
|
38
|
+
clearcutIncludePidHeader: options.clearcutIncludePidHeader,
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
async logToolInvocation(args) {
|
|
@@ -5,44 +5,197 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import { logger } from '../../logger.js';
|
|
8
|
+
const MAX_BUFFER_SIZE = 1000;
|
|
9
|
+
const DEFAULT_CLEARCUT_ENDPOINT = 'https://play.googleapis.com/log?format=json_proto';
|
|
10
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 15 * 60 * 1000;
|
|
11
|
+
const LOG_SOURCE = 2839;
|
|
12
|
+
const CLIENT_TYPE = 47;
|
|
13
|
+
const MIN_RATE_LIMIT_WAIT_MS = 30_000;
|
|
14
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
15
|
+
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
8
16
|
const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9
17
|
export class ClearcutSender {
|
|
10
18
|
#appVersion;
|
|
11
19
|
#osType;
|
|
20
|
+
#clearcutEndpoint;
|
|
21
|
+
#flushIntervalMs;
|
|
22
|
+
#includePidHeader;
|
|
12
23
|
#sessionId;
|
|
13
24
|
#sessionCreated;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
#buffer = [];
|
|
26
|
+
#flushTimer = null;
|
|
27
|
+
#isFlushing = false;
|
|
28
|
+
#timerStarted = false;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.#appVersion = config.appVersion;
|
|
31
|
+
this.#osType = config.osType;
|
|
32
|
+
this.#clearcutEndpoint =
|
|
33
|
+
config.clearcutEndpoint ?? DEFAULT_CLEARCUT_ENDPOINT;
|
|
34
|
+
this.#flushIntervalMs =
|
|
35
|
+
config.forceFlushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
36
|
+
this.#includePidHeader = config.includePidHeader ?? false;
|
|
17
37
|
this.#sessionId = crypto.randomUUID();
|
|
18
38
|
this.#sessionCreated = Date.now();
|
|
19
39
|
}
|
|
20
|
-
|
|
21
|
-
this.#rotateSessionIfNeeded();
|
|
22
|
-
const enrichedEvent = this.#enrichEvent(event);
|
|
23
|
-
this.transport(enrichedEvent);
|
|
24
|
-
}
|
|
25
|
-
transport(event) {
|
|
26
|
-
logger('Telemetry event', JSON.stringify(event, null, 2));
|
|
27
|
-
}
|
|
28
|
-
async sendShutdownEvent() {
|
|
29
|
-
const shutdownEvent = {
|
|
30
|
-
server_shutdown: {},
|
|
31
|
-
};
|
|
32
|
-
await this.send(shutdownEvent);
|
|
33
|
-
}
|
|
34
|
-
#rotateSessionIfNeeded() {
|
|
40
|
+
enqueueEvent(event) {
|
|
35
41
|
if (Date.now() - this.#sessionCreated > SESSION_ROTATION_INTERVAL_MS) {
|
|
36
42
|
this.#sessionId = crypto.randomUUID();
|
|
37
43
|
this.#sessionCreated = Date.now();
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return {
|
|
45
|
+
logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
|
|
46
|
+
this.#addToBuffer({
|
|
42
47
|
...event,
|
|
43
48
|
session_id: this.#sessionId,
|
|
44
49
|
app_version: this.#appVersion,
|
|
45
50
|
os_type: this.#osType,
|
|
51
|
+
});
|
|
52
|
+
if (!this.#timerStarted) {
|
|
53
|
+
this.#timerStarted = true;
|
|
54
|
+
this.#scheduleFlush(this.#flushIntervalMs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async sendShutdownEvent() {
|
|
58
|
+
if (this.#flushTimer) {
|
|
59
|
+
clearTimeout(this.#flushTimer);
|
|
60
|
+
this.#flushTimer = null;
|
|
61
|
+
}
|
|
62
|
+
const shutdownEvent = {
|
|
63
|
+
server_shutdown: {},
|
|
46
64
|
};
|
|
65
|
+
this.enqueueEvent(shutdownEvent);
|
|
66
|
+
try {
|
|
67
|
+
await Promise.race([
|
|
68
|
+
this.#finalFlush(),
|
|
69
|
+
new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
|
|
70
|
+
]);
|
|
71
|
+
logger('Final flush completed');
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger('Final flush failed:', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async #flush() {
|
|
78
|
+
if (this.#isFlushing) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.#buffer.length === 0) {
|
|
82
|
+
this.#scheduleFlush(this.#flushIntervalMs);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.#isFlushing = true;
|
|
86
|
+
let nextDelayMs = this.#flushIntervalMs;
|
|
87
|
+
// Optimistically remove events from buffer before sending.
|
|
88
|
+
// This prevents race conditions where a simultaneous #finalFlush would include these same events.
|
|
89
|
+
const eventsToSend = [...this.#buffer];
|
|
90
|
+
this.#buffer = [];
|
|
91
|
+
try {
|
|
92
|
+
const result = await this.#sendBatch(eventsToSend);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
if (result.nextRequestWaitMs !== undefined) {
|
|
95
|
+
nextDelayMs = Math.max(result.nextRequestWaitMs, MIN_RATE_LIMIT_WAIT_MS);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (result.isPermanentError) {
|
|
99
|
+
logger('Permanent error, dropped batch of', eventsToSend.length, 'events');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Transient error: Requeue events at the front of the buffer
|
|
103
|
+
// to maintain order and retry them later.
|
|
104
|
+
this.#buffer = [...eventsToSend, ...this.#buffer];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// Safety catch for unexpected errors, requeue events
|
|
109
|
+
this.#buffer = [...eventsToSend, ...this.#buffer];
|
|
110
|
+
logger('Flush failed unexpectedly:', error);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.#isFlushing = false;
|
|
114
|
+
this.#scheduleFlush(nextDelayMs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
#addToBuffer(event) {
|
|
118
|
+
if (this.#buffer.length >= MAX_BUFFER_SIZE) {
|
|
119
|
+
this.#buffer.shift();
|
|
120
|
+
logger('Telemetry buffer overflow: dropped oldest event');
|
|
121
|
+
}
|
|
122
|
+
this.#buffer.push({
|
|
123
|
+
event,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
#scheduleFlush(delayMs) {
|
|
128
|
+
if (this.#flushTimer) {
|
|
129
|
+
clearTimeout(this.#flushTimer);
|
|
130
|
+
}
|
|
131
|
+
this.#flushTimer = setTimeout(() => {
|
|
132
|
+
this.#flush().catch(err => {
|
|
133
|
+
logger('Flush error:', err);
|
|
134
|
+
});
|
|
135
|
+
}, delayMs);
|
|
136
|
+
}
|
|
137
|
+
async #sendBatch(events) {
|
|
138
|
+
const requestBody = {
|
|
139
|
+
log_source: LOG_SOURCE,
|
|
140
|
+
request_time_ms: Date.now().toString(),
|
|
141
|
+
client_info: {
|
|
142
|
+
client_type: CLIENT_TYPE,
|
|
143
|
+
},
|
|
144
|
+
log_event: events.map(({ event, timestamp }) => ({
|
|
145
|
+
event_time_ms: timestamp.toString(),
|
|
146
|
+
source_extension_json: JSON.stringify(event),
|
|
147
|
+
})),
|
|
148
|
+
};
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(this.#clearcutEndpoint, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
// Used in E2E tests to confirm that the watchdog process is killed
|
|
157
|
+
...(this.#includePidHeader
|
|
158
|
+
? { 'X-Watchdog-Pid': process.pid.toString() }
|
|
159
|
+
: {}),
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(requestBody),
|
|
162
|
+
signal: controller.signal,
|
|
163
|
+
});
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
if (response.ok) {
|
|
166
|
+
const data = (await response.json());
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
nextRequestWaitMs: data.next_request_wait_millis,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const status = response.status;
|
|
173
|
+
if (status >= 500 || status === 429) {
|
|
174
|
+
return { success: false };
|
|
175
|
+
}
|
|
176
|
+
logger('Telemetry permanent error:', status);
|
|
177
|
+
return { success: false, isPermanentError: true };
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
return { success: false };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async #finalFlush() {
|
|
185
|
+
if (this.#buffer.length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const eventsToSend = [...this.#buffer];
|
|
189
|
+
await this.#sendBatch(eventsToSend);
|
|
190
|
+
}
|
|
191
|
+
stopForTesting() {
|
|
192
|
+
if (this.#flushTimer) {
|
|
193
|
+
clearTimeout(this.#flushTimer);
|
|
194
|
+
this.#flushTimer = null;
|
|
195
|
+
}
|
|
196
|
+
this.#timerStarted = false;
|
|
197
|
+
}
|
|
198
|
+
get bufferSizeForTesting() {
|
|
199
|
+
return this.#buffer.length;
|
|
47
200
|
}
|
|
48
201
|
}
|
|
@@ -9,20 +9,50 @@ import { parseArgs } from 'node:util';
|
|
|
9
9
|
import { logger, flushLogs, saveLogsToFile } from '../../logger.js';
|
|
10
10
|
import { WatchdogMessageType } from '../types.js';
|
|
11
11
|
import { ClearcutSender } from './clearcut-sender.js';
|
|
12
|
-
function
|
|
12
|
+
function parseWatchdogArgs() {
|
|
13
13
|
const { values } = parseArgs({
|
|
14
14
|
options: {
|
|
15
15
|
'parent-pid': { type: 'string' },
|
|
16
16
|
'app-version': { type: 'string' },
|
|
17
17
|
'os-type': { type: 'string' },
|
|
18
18
|
'log-file': { type: 'string' },
|
|
19
|
+
'clearcut-endpoint': { type: 'string' },
|
|
20
|
+
'clearcut-force-flush-interval-ms': { type: 'string' },
|
|
21
|
+
'clearcut-include-pid-header': { type: 'boolean' },
|
|
19
22
|
},
|
|
20
23
|
strict: true,
|
|
21
24
|
});
|
|
25
|
+
// Verify required arguments
|
|
22
26
|
const parentPid = parseInt(values['parent-pid'] ?? '', 10);
|
|
23
27
|
const appVersion = values['app-version'];
|
|
24
28
|
const osType = parseInt(values['os-type'] ?? '', 10);
|
|
29
|
+
if (isNaN(parentPid) || !appVersion || isNaN(osType)) {
|
|
30
|
+
console.error('Invalid arguments provided for watchdog process: ', JSON.stringify({ parentPid, appVersion, osType }));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Parse Optional Arguments
|
|
25
34
|
const logFile = values['log-file'];
|
|
35
|
+
const clearcutEndpoint = values['clearcut-endpoint'];
|
|
36
|
+
const clearcutIncludePidHeader = values['clearcut-include-pid-header'];
|
|
37
|
+
let clearcutForceFlushIntervalMs;
|
|
38
|
+
if (values['clearcut-force-flush-interval-ms']) {
|
|
39
|
+
const parsed = parseInt(values['clearcut-force-flush-interval-ms'], 10);
|
|
40
|
+
if (!isNaN(parsed)) {
|
|
41
|
+
clearcutForceFlushIntervalMs = parsed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
parentPid,
|
|
46
|
+
appVersion,
|
|
47
|
+
osType,
|
|
48
|
+
logFile,
|
|
49
|
+
clearcutEndpoint,
|
|
50
|
+
clearcutForceFlushIntervalMs,
|
|
51
|
+
clearcutIncludePidHeader,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function main() {
|
|
55
|
+
const { parentPid, appVersion, osType, logFile, clearcutEndpoint, clearcutForceFlushIntervalMs, clearcutIncludePidHeader, } = parseWatchdogArgs();
|
|
26
56
|
let logStream;
|
|
27
57
|
if (logFile) {
|
|
28
58
|
logStream = saveLogsToFile(logFile);
|
|
@@ -35,18 +65,19 @@ function main() {
|
|
|
35
65
|
process.exit(code);
|
|
36
66
|
});
|
|
37
67
|
};
|
|
38
|
-
if (isNaN(parentPid) || !appVersion || isNaN(osType)) {
|
|
39
|
-
logger('Invalid arguments provided for watchdog process: ', JSON.stringify({ parentPid, appVersion, osType }));
|
|
40
|
-
exit(1);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
68
|
logger('Watchdog started', JSON.stringify({
|
|
44
69
|
pid: process.pid,
|
|
45
70
|
parentPid,
|
|
46
71
|
version: appVersion,
|
|
47
72
|
osType,
|
|
48
73
|
}, null, 2));
|
|
49
|
-
const sender = new ClearcutSender(
|
|
74
|
+
const sender = new ClearcutSender({
|
|
75
|
+
appVersion,
|
|
76
|
+
osType: osType,
|
|
77
|
+
clearcutEndpoint,
|
|
78
|
+
forceFlushIntervalMs: clearcutForceFlushIntervalMs,
|
|
79
|
+
includePidHeader: clearcutIncludePidHeader,
|
|
80
|
+
});
|
|
50
81
|
let isShuttingDown = false;
|
|
51
82
|
function onParentDeath(reason) {
|
|
52
83
|
if (isShuttingDown) {
|
|
@@ -79,9 +110,7 @@ function main() {
|
|
|
79
110
|
}
|
|
80
111
|
const msg = JSON.parse(line);
|
|
81
112
|
if (msg.type === WatchdogMessageType.LOG_EVENT && msg.payload) {
|
|
82
|
-
sender.
|
|
83
|
-
logger('Error sending event', err);
|
|
84
|
-
});
|
|
113
|
+
sender.enqueueEvent(msg.payload);
|
|
85
114
|
}
|
|
86
115
|
}
|
|
87
116
|
catch (err) {
|
|
@@ -19,6 +19,15 @@ export class WatchdogClient {
|
|
|
19
19
|
if (config.logFile) {
|
|
20
20
|
args.push(`--log-file=${config.logFile}`);
|
|
21
21
|
}
|
|
22
|
+
if (config.clearcutEndpoint) {
|
|
23
|
+
args.push(`--clearcut-endpoint=${config.clearcutEndpoint}`);
|
|
24
|
+
}
|
|
25
|
+
if (config.clearcutForceFlushIntervalMs) {
|
|
26
|
+
args.push(`--clearcut-force-flush-interval-ms=${config.clearcutForceFlushIntervalMs}`);
|
|
27
|
+
}
|
|
28
|
+
if (config.clearcutIncludePidHeader) {
|
|
29
|
+
args.push('--clearcut-include-pid-header');
|
|
30
|
+
}
|
|
22
31
|
const spawner = options?.spawn ?? spawn;
|
|
23
32
|
this.#childProcess = spawner(process.execPath, args, {
|
|
24
33
|
stdio: ['pipe', 'ignore', 'ignore'],
|