@mcp-b/chrome-devtools-mcp 1.7.0 → 1.7.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 +120 -10
- package/build/src/McpContext.js +50 -3
- package/build/src/browser.js +62 -6
- package/build/src/cli.js +6 -1
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/main.js +83 -2
- package/build/src/telemetry/clearcut-logger.js +102 -0
- package/build/src/telemetry/flag-utils.js +45 -0
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +33 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
- package/build/src/telemetry/watchdog/main.js +127 -0
- package/build/src/telemetry/watchdog-client.js +60 -0
- package/build/src/third_party/devtools-formatter-worker.js +7 -0
- package/build/src/tools/browser.js +92 -0
- package/build/src/tools/extension.js +31 -0
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/input.js +6 -1
- package/build/src/tools/pages.js +0 -1
- package/build/src/tools/tools.js +4 -0
- package/build/src/transports/WebMCPBridgeScript.js +11 -2
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/build/src/utils/string.js +36 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Base64.js +20 -2
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Debouncer.js +8 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Gzip.js +11 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Object.js +6 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/ParsedURL.js +3 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/ResourceType.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Revealer.js +0 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Settings.js +18 -8
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/AidaClient.js +24 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +11 -3
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +27 -20
- package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/collect-ui-strings.js +7 -8
- package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/generate-locales-js.js +4 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +10 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +63 -12
- package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/CDPConnection.js +1 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +4 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/root/ExperimentNames.js +30 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/root/root.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/AnimationModel.js +0 -4
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +69 -9
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +6 -6
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +28 -13
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSProperty.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ConsoleModel.js +0 -2
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CookieModel.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +170 -13
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +5 -39
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/HeapProfilerModel.js +8 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +20 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +12 -21
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/OverlayModel.js +19 -6
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +8 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +15 -10
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +13 -27
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/Target.js +3 -1
- package/build/vendor/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1 -7
- package/build/vendor/chrome-devtools-frontend/front_end/generated/Deprecation.js +1 -16
- package/build/vendor/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +82 -22
- package/build/vendor/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +265 -123
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -16
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +97 -26
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +35 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationRepository.js +163 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationType.js +10 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/annotations.js +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +5 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +29 -58
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +7 -45
- package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +14 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +8 -5
- package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/Prototypes.js +33 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/greendev.js +4 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/ContrastCheckTrigger.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CookieIssue.js +0 -21
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CorsIssue.js +1 -38
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssueAggregator.js +8 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssuesManager.js +6 -12
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/PermissionElementIssue.js +243 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabled.md +7 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluder.md +9 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluderParent.md +9 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementCspFrameAncestorsMissing.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFencedFrameDisallowed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooLarge.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooSmall.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementGeolocationDeprecated.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInsetBoxShadowUnsupported.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidDisplayStyle.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidSizeValue.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidType.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidTypeActivation.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementLowContrast.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementNonOpaqueColor.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingBottomUnsupported.md +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingRightUnsupported.md +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPermissionsPolicyBlocked.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRegistrationFailed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRequestInProgress.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementSecurityChecksFailed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementTypeNotSupported.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementUntrustedEvent.md +7 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/issues_manager.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/logs/NetworkLog.js +0 -8
- package/build/vendor/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +4 -8
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +30 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +70 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +82 -30
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +10 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +0 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Processor.js +18 -19
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Styles.js +12 -4
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/Initiators.js +46 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +4 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/extras.js +1 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LargestImagePaintHandler.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +10 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/PageLoadMetricsHandler.js +44 -27
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +9 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/Common.js +1 -6
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -4
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +3 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +33 -11
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +51 -18
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/scopes.js +4 -0
- package/build/vendor/chrome-devtools-frontend/mcp/HostBindings.js +4 -0
- package/build/vendor/chrome-devtools-frontend/mcp/mcp.js +4 -0
- package/package.json +17 -10
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteInvalidSameParty.md +0 -8
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
package/build/src/main.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import './polyfill.js';
|
|
8
|
-
import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
|
|
8
|
+
import { connectToNewBrowser, disconnectBrowser, ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
|
|
9
9
|
import { parseArguments } from './cli.js';
|
|
10
10
|
import { loadIssueDescriptions } from './issue-descriptions.js';
|
|
11
11
|
import { logger, saveLogsToFile } from './logger.js';
|
|
@@ -102,6 +102,7 @@ async function getContext() {
|
|
|
102
102
|
devtools,
|
|
103
103
|
channel: undefined,
|
|
104
104
|
userDataDir: args.userDataDir,
|
|
105
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
// If autoConnect is true, try connecting first, then fall back to launching
|
|
@@ -115,6 +116,7 @@ async function getContext() {
|
|
|
115
116
|
devtools,
|
|
116
117
|
channel: args.channel,
|
|
117
118
|
userDataDir: args.userDataDir,
|
|
119
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
118
120
|
});
|
|
119
121
|
logger('Successfully connected to running browser instance');
|
|
120
122
|
}
|
|
@@ -131,6 +133,7 @@ async function getContext() {
|
|
|
131
133
|
args: extraArgs,
|
|
132
134
|
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
133
135
|
devtools,
|
|
136
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
134
137
|
});
|
|
135
138
|
wasLaunched = true;
|
|
136
139
|
}
|
|
@@ -148,6 +151,7 @@ async function getContext() {
|
|
|
148
151
|
args: extraArgs,
|
|
149
152
|
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
150
153
|
devtools,
|
|
154
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
151
155
|
});
|
|
152
156
|
wasLaunched = true;
|
|
153
157
|
}
|
|
@@ -155,6 +159,8 @@ async function getContext() {
|
|
|
155
159
|
context = await McpContext.from(browser, logger, {
|
|
156
160
|
experimentalDevToolsDebugging: devtools,
|
|
157
161
|
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
|
|
162
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
163
|
+
onReconnect,
|
|
158
164
|
});
|
|
159
165
|
if (wasLaunched) {
|
|
160
166
|
// Fresh browser launch - use the existing default page
|
|
@@ -201,6 +207,77 @@ async function getContext() {
|
|
|
201
207
|
}
|
|
202
208
|
return context;
|
|
203
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Reconnect callback invoked by the connect_to_browser tool.
|
|
212
|
+
*
|
|
213
|
+
* Tears down the current browser session and connects to a new browser,
|
|
214
|
+
* rebuilding all infrastructure (McpContext, WebMCPToolHub, session window).
|
|
215
|
+
* Retries connection with exponential backoff for browsers that are still starting.
|
|
216
|
+
*/
|
|
217
|
+
async function onReconnect(options) {
|
|
218
|
+
const timeout = options.timeout ?? 30_000;
|
|
219
|
+
const retryInterval = 2_000;
|
|
220
|
+
const devtools = args.experimentalDevtools ?? false;
|
|
221
|
+
// 1. Tear down old session
|
|
222
|
+
if (context) {
|
|
223
|
+
try {
|
|
224
|
+
await context.closeSessionWindow();
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
logger('Error closing session window during reconnect:', err);
|
|
228
|
+
}
|
|
229
|
+
context.dispose();
|
|
230
|
+
}
|
|
231
|
+
disconnectBrowser();
|
|
232
|
+
// 2. Retry loop to connect to new browser
|
|
233
|
+
const deadline = Date.now() + timeout;
|
|
234
|
+
let lastError;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
try {
|
|
237
|
+
const newBrowser = await connectToNewBrowser({
|
|
238
|
+
browserURL: options.browserURL,
|
|
239
|
+
wsEndpoint: options.wsEndpoint,
|
|
240
|
+
wsHeaders: options.wsHeaders,
|
|
241
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
242
|
+
});
|
|
243
|
+
// 3. Rebuild context
|
|
244
|
+
context = await McpContext.from(newBrowser, logger, {
|
|
245
|
+
experimentalDevToolsDebugging: devtools,
|
|
246
|
+
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
|
|
247
|
+
includeExtensionPages: args.includeExtensionPages,
|
|
248
|
+
onReconnect,
|
|
249
|
+
});
|
|
250
|
+
// 4. Create session window
|
|
251
|
+
const { windowId } = await context.newWindow();
|
|
252
|
+
context.setSessionWindowId(windowId);
|
|
253
|
+
logger(`Reconnect: new session window ${windowId}`);
|
|
254
|
+
// 5. Rebuild tool hub
|
|
255
|
+
const toolHub = new WebMCPToolHub(server, context);
|
|
256
|
+
context.setToolHub(toolHub);
|
|
257
|
+
logger('Reconnect: WebMCPToolHub rebuilt');
|
|
258
|
+
// 6. Build response
|
|
259
|
+
const pages = context.getPages().map((page, index) => ({
|
|
260
|
+
index,
|
|
261
|
+
url: page.url(),
|
|
262
|
+
selected: context.isPageSelected(page),
|
|
263
|
+
}));
|
|
264
|
+
const wsEndpoint = newBrowser.wsEndpoint();
|
|
265
|
+
return { pages, wsEndpoint };
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
269
|
+
logger(`Reconnect attempt failed: ${lastError.message}`);
|
|
270
|
+
const remaining = deadline - Date.now();
|
|
271
|
+
if (remaining > retryInterval) {
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, retryInterval));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`Failed to connect to browser after ${timeout}ms: ${lastError?.message ?? 'unknown error'}`);
|
|
280
|
+
}
|
|
204
281
|
/**
|
|
205
282
|
* Log security disclaimers to stderr.
|
|
206
283
|
*
|
|
@@ -251,7 +328,11 @@ function registerTool(tool) {
|
|
|
251
328
|
await tool.handler({
|
|
252
329
|
params,
|
|
253
330
|
}, response, context);
|
|
254
|
-
|
|
331
|
+
// Re-fetch context in case the handler swapped it (e.g. connect_to_browser).
|
|
332
|
+
// The local `const context` shadows the module-level `let context`, so we
|
|
333
|
+
// must call getContext() to pick up any reassignment by onReconnect.
|
|
334
|
+
const activeContext = await getContext();
|
|
335
|
+
const content = await response.handle(tool.name, activeContext);
|
|
255
336
|
const result = {
|
|
256
337
|
content,
|
|
257
338
|
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
import { FilePersistence } from './persistence.js';
|
|
9
|
+
import { WatchdogMessageType, OsType } from './types.js';
|
|
10
|
+
import { WatchdogClient } from './watchdog-client.js';
|
|
11
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
12
|
+
function detectOsType() {
|
|
13
|
+
switch (process.platform) {
|
|
14
|
+
case 'win32':
|
|
15
|
+
return OsType.OS_TYPE_WINDOWS;
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return OsType.OS_TYPE_MACOS;
|
|
18
|
+
case 'linux':
|
|
19
|
+
return OsType.OS_TYPE_LINUX;
|
|
20
|
+
default:
|
|
21
|
+
return OsType.OS_TYPE_UNSPECIFIED;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ClearcutLogger {
|
|
25
|
+
#persistence;
|
|
26
|
+
#watchdog;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#persistence = options.persistence ?? new FilePersistence();
|
|
29
|
+
this.#watchdog =
|
|
30
|
+
options.watchdogClient ??
|
|
31
|
+
new WatchdogClient({
|
|
32
|
+
parentPid: process.pid,
|
|
33
|
+
appVersion: options.appVersion,
|
|
34
|
+
osType: detectOsType(),
|
|
35
|
+
logFile: options.logFile,
|
|
36
|
+
clearcutEndpoint: options.clearcutEndpoint,
|
|
37
|
+
clearcutForceFlushIntervalMs: options.clearcutForceFlushIntervalMs,
|
|
38
|
+
clearcutIncludePidHeader: options.clearcutIncludePidHeader,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async logToolInvocation(args) {
|
|
42
|
+
this.#watchdog.send({
|
|
43
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
44
|
+
payload: {
|
|
45
|
+
tool_invocation: {
|
|
46
|
+
tool_name: args.toolName,
|
|
47
|
+
success: args.success,
|
|
48
|
+
latency_ms: args.latencyMs,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async logServerStart(flagUsage) {
|
|
54
|
+
this.#watchdog.send({
|
|
55
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
56
|
+
payload: {
|
|
57
|
+
server_start: {
|
|
58
|
+
flag_usage: flagUsage,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async logDailyActiveIfNeeded() {
|
|
64
|
+
try {
|
|
65
|
+
const state = await this.#persistence.loadState();
|
|
66
|
+
if (this.#shouldLogDailyActive(state)) {
|
|
67
|
+
let daysSince = -1;
|
|
68
|
+
if (state.lastActive) {
|
|
69
|
+
const lastActiveDate = new Date(state.lastActive);
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const diffTime = Math.abs(now.getTime() - lastActiveDate.getTime());
|
|
72
|
+
daysSince = Math.ceil(diffTime / MS_PER_DAY);
|
|
73
|
+
}
|
|
74
|
+
this.#watchdog.send({
|
|
75
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
76
|
+
payload: {
|
|
77
|
+
daily_active: {
|
|
78
|
+
days_since_last_active: daysSince,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
state.lastActive = new Date().toISOString();
|
|
83
|
+
await this.#persistence.saveState(state);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger('Error in logDailyActiveIfNeeded:', err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
#shouldLogDailyActive(state) {
|
|
91
|
+
if (!state.lastActive) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const lastActiveDate = new Date(state.lastActive);
|
|
95
|
+
const now = new Date();
|
|
96
|
+
// Compare UTC dates
|
|
97
|
+
const isSameDay = lastActiveDate.getUTCFullYear() === now.getUTCFullYear() &&
|
|
98
|
+
lastActiveDate.getUTCMonth() === now.getUTCMonth() &&
|
|
99
|
+
lastActiveDate.getUTCDate() === now.getUTCDate();
|
|
100
|
+
return !isSameDay;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { toSnakeCase } from '../utils/string.js';
|
|
7
|
+
/**
|
|
8
|
+
* Computes telemetry flag usage from parsed arguments and CLI options.
|
|
9
|
+
*
|
|
10
|
+
* Iterates over the defined CLI options to construct a payload:
|
|
11
|
+
* - Flag names are converted to snake_case (e.g. `browserUrl` -> `browser_url`).
|
|
12
|
+
* - A flag is logged as `{flag_name}_present` if:
|
|
13
|
+
* - It has no default value, OR
|
|
14
|
+
* - The provided value differs from the default value.
|
|
15
|
+
* - Boolean flags are logged with their literal value.
|
|
16
|
+
* - String flags with defined `choices` (Enums) are logged as their uppercase value.
|
|
17
|
+
*/
|
|
18
|
+
export function computeFlagUsage(args, options) {
|
|
19
|
+
const usage = {};
|
|
20
|
+
for (const [flagName, config] of Object.entries(options)) {
|
|
21
|
+
const value = args[flagName];
|
|
22
|
+
const snakeCaseName = toSnakeCase(flagName);
|
|
23
|
+
// If there isn't a default value provided for the flag,
|
|
24
|
+
// we're going to log whether it's present on the args user
|
|
25
|
+
// provided or not. If there is a default value, we only log presence
|
|
26
|
+
// if the value differs from the default, implying explicit user intent.
|
|
27
|
+
if (!('default' in config) || value !== config.default) {
|
|
28
|
+
usage[`${snakeCaseName}_present`] = value !== undefined && value !== null;
|
|
29
|
+
}
|
|
30
|
+
if (config.type === 'boolean' && typeof value === 'boolean') {
|
|
31
|
+
// For boolean options, we're going to log the value directly.
|
|
32
|
+
usage[snakeCaseName] = value;
|
|
33
|
+
}
|
|
34
|
+
else if (config.type === 'string' &&
|
|
35
|
+
typeof value === 'string' &&
|
|
36
|
+
'choices' in config &&
|
|
37
|
+
config.choices) {
|
|
38
|
+
// For enums, log the value as uppercase
|
|
39
|
+
// We're going to have an enum for such flags with choices represented
|
|
40
|
+
// as an `enum` where the keys of the enum will map to the uppercase `choice`.
|
|
41
|
+
usage[snakeCaseName] = `${snakeCaseName}_${value}`.toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return usage;
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
const LATENCY_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000];
|
|
7
|
+
export function bucketizeLatency(latencyMs) {
|
|
8
|
+
for (const bucket of LATENCY_BUCKETS) {
|
|
9
|
+
if (latencyMs <= bucket) {
|
|
10
|
+
return bucket;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1];
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { logger } from '../logger.js';
|
|
11
|
+
const STATE_FILE_NAME = 'telemetry_state.json';
|
|
12
|
+
function getDataFolder() {
|
|
13
|
+
const homedir = os.homedir();
|
|
14
|
+
const { env } = process;
|
|
15
|
+
const name = 'chrome-devtools-mcp';
|
|
16
|
+
if (process.platform === 'darwin') {
|
|
17
|
+
return path.join(homedir, 'Library', 'Application Support', name);
|
|
18
|
+
}
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
const localAppData = env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
|
|
21
|
+
return path.join(localAppData, name, 'Data');
|
|
22
|
+
}
|
|
23
|
+
return path.join(env.XDG_DATA_HOME || path.join(homedir, '.local', 'share'), name);
|
|
24
|
+
}
|
|
25
|
+
export class FilePersistence {
|
|
26
|
+
#dataFolder;
|
|
27
|
+
constructor(dataFolderOverride) {
|
|
28
|
+
this.#dataFolder = dataFolderOverride ?? getDataFolder();
|
|
29
|
+
}
|
|
30
|
+
async loadState() {
|
|
31
|
+
try {
|
|
32
|
+
const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
|
|
33
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {
|
|
38
|
+
lastActive: '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async saveState(state) {
|
|
43
|
+
const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
|
|
44
|
+
try {
|
|
45
|
+
await fs.mkdir(this.#dataFolder, { recursive: true });
|
|
46
|
+
await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Ignore errors during state saving to avoid crashing the server
|
|
50
|
+
logger(`Failed to save telemetry state to ${filePath}:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
// Enums
|
|
7
|
+
export var OsType;
|
|
8
|
+
(function (OsType) {
|
|
9
|
+
OsType[OsType["OS_TYPE_UNSPECIFIED"] = 0] = "OS_TYPE_UNSPECIFIED";
|
|
10
|
+
OsType[OsType["OS_TYPE_WINDOWS"] = 1] = "OS_TYPE_WINDOWS";
|
|
11
|
+
OsType[OsType["OS_TYPE_MACOS"] = 2] = "OS_TYPE_MACOS";
|
|
12
|
+
OsType[OsType["OS_TYPE_LINUX"] = 3] = "OS_TYPE_LINUX";
|
|
13
|
+
})(OsType || (OsType = {}));
|
|
14
|
+
export var ChromeChannel;
|
|
15
|
+
(function (ChromeChannel) {
|
|
16
|
+
ChromeChannel[ChromeChannel["CHROME_CHANNEL_UNSPECIFIED"] = 0] = "CHROME_CHANNEL_UNSPECIFIED";
|
|
17
|
+
ChromeChannel[ChromeChannel["CHROME_CHANNEL_CANARY"] = 1] = "CHROME_CHANNEL_CANARY";
|
|
18
|
+
ChromeChannel[ChromeChannel["CHROME_CHANNEL_DEV"] = 2] = "CHROME_CHANNEL_DEV";
|
|
19
|
+
ChromeChannel[ChromeChannel["CHROME_CHANNEL_BETA"] = 3] = "CHROME_CHANNEL_BETA";
|
|
20
|
+
ChromeChannel[ChromeChannel["CHROME_CHANNEL_STABLE"] = 4] = "CHROME_CHANNEL_STABLE";
|
|
21
|
+
})(ChromeChannel || (ChromeChannel = {}));
|
|
22
|
+
export var McpClient;
|
|
23
|
+
(function (McpClient) {
|
|
24
|
+
McpClient[McpClient["MCP_CLIENT_UNSPECIFIED"] = 0] = "MCP_CLIENT_UNSPECIFIED";
|
|
25
|
+
McpClient[McpClient["MCP_CLIENT_CLAUDE_CODE"] = 1] = "MCP_CLIENT_CLAUDE_CODE";
|
|
26
|
+
McpClient[McpClient["MCP_CLIENT_GEMINI_CLI"] = 2] = "MCP_CLIENT_GEMINI_CLI";
|
|
27
|
+
})(McpClient || (McpClient = {}));
|
|
28
|
+
// IPC types for messages between the main process and the
|
|
29
|
+
// telemetry watchdog process.
|
|
30
|
+
export var WatchdogMessageType;
|
|
31
|
+
(function (WatchdogMessageType) {
|
|
32
|
+
WatchdogMessageType["LOG_EVENT"] = "log-event";
|
|
33
|
+
})(WatchdogMessageType || (WatchdogMessageType = {}));
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import crypto from 'node:crypto';
|
|
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;
|
|
16
|
+
const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
export class ClearcutSender {
|
|
18
|
+
#appVersion;
|
|
19
|
+
#osType;
|
|
20
|
+
#clearcutEndpoint;
|
|
21
|
+
#flushIntervalMs;
|
|
22
|
+
#includePidHeader;
|
|
23
|
+
#sessionId;
|
|
24
|
+
#sessionCreated;
|
|
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;
|
|
37
|
+
this.#sessionId = crypto.randomUUID();
|
|
38
|
+
this.#sessionCreated = Date.now();
|
|
39
|
+
}
|
|
40
|
+
enqueueEvent(event) {
|
|
41
|
+
if (Date.now() - this.#sessionCreated > SESSION_ROTATION_INTERVAL_MS) {
|
|
42
|
+
this.#sessionId = crypto.randomUUID();
|
|
43
|
+
this.#sessionCreated = Date.now();
|
|
44
|
+
}
|
|
45
|
+
logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
|
|
46
|
+
this.#addToBuffer({
|
|
47
|
+
...event,
|
|
48
|
+
session_id: this.#sessionId,
|
|
49
|
+
app_version: this.#appVersion,
|
|
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: {},
|
|
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;
|
|
200
|
+
}
|
|
201
|
+
}
|