项目文件夹

文件
Ergün Erdoğmuş ee35f207eb chore: Implement Watchdog process for reliable telemetry (#777)
This PR implements the watchdog process architecture for the telemetry
system. It moves the `ClearcutSender` execution to a dedicated child
process, ensuring that events—especially shutdown events—are reliably
transmitted even if the main server process terminates abruptly.

Added an e2e test that runs the server, checks the log file and confirms
the telemetry logs exist and that the watchdog process is correctly
killed after sending the shutdown event once the main process is killed.

**Implementation Roadmap:**
This is the fourth in a series of PRs designed to implement the
telemetry system:
1. **CLI & Opt-out Mechanism
([Merged](https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/757)):**
    *   Added `--usage-statistics` flag and transparency logging.
2. **Logger Scaffolding & Integration
([Merged](https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/758)):**
    *   **`ClearcutLogger`**: Implemented the main logging entry point.
* **One-way Data Flow**: Integrated `logToolInvocation` and
`logServerStart` hooks into `main.ts` to capture events.
    *   **`ClearcutSender`**: Introduced a transport abstraction.
* **Type Definitions**: Added TypeScript definitions for the telemetry
Protocol Buffer messages.
3. **Persistence Layer
([Merged](https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/766)):**
* **`FilePersistence`**: Implemented a local file-based state manager to
persist the `lastActive` timestamp.
* **Daily Active Logic**: Integrated persistence into `ClearcutLogger`
to automatically detect and log `daily_active` events (with
`days_since_last_active` calculation) via `logDailyActiveIfNeeded`.
4.  **Watchdog Process Architecture (This PR):**
* **`WatchdogClient`**: Added a client-side wrapper to spawn and
communicate with the watchdog process via `stdin`.
* **`watchdog/main.ts`**: Created the entry point for the watchdog
process. It listens for IPC messages and uses `ClearcutSender` to
transmit events.
* **Reliable Shutdown**: The watchdog monitors the parent process and
guarantees a `shutdown` event is sent when the parent exits or crashes
(detecting `stdin` closure).
* **Refactoring**: Moved `ClearcutSender` to the `watchdog` directory
and updated `ClearcutLogger` to delegate event sending to the
`WatchdogClient`.
5.  **Transport, Batching & Retries (Next):**
* Finalize `ClearcutSender` with actual HTTP transport logic, including
event batching and exponential backoff retries.
2026-01-19 13:38:58 +00:00

137 行
4.2 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {describe, it, afterEach, beforeEach} from 'node:test';
import sinon from 'sinon';
import {ClearcutLogger} from '../../src/telemetry/clearcut-logger.js';
import type {Persistence} from '../../src/telemetry/persistence.js';
import {FilePersistence} from '../../src/telemetry/persistence.js';
import {WatchdogMessageType} from '../../src/telemetry/types.js';
import {WatchdogClient} from '../../src/telemetry/watchdog-client.js';
describe('ClearcutLogger', () => {
let mockPersistence: sinon.SinonStubbedInstance<Persistence>;
let mockWatchdogClient: sinon.SinonStubbedInstance<WatchdogClient>;
beforeEach(() => {
mockPersistence = sinon.createStubInstance(FilePersistence, {
loadState: Promise.resolve({
lastActive: '',
}),
});
mockWatchdogClient = sinon.createStubInstance(WatchdogClient);
});
afterEach(() => {
sinon.restore();
});
describe('logToolInvocation', () => {
it('sends correct payload', async () => {
const logger = new ClearcutLogger({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
await logger.logToolInvocation({
toolName: 'test_tool',
success: true,
latencyMs: 123,
});
assert(mockWatchdogClient.send.calledOnce);
const msg = mockWatchdogClient.send.firstCall.args[0];
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
assert.strictEqual(msg.payload.tool_invocation?.tool_name, 'test_tool');
assert.strictEqual(msg.payload.tool_invocation?.success, true);
assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
});
});
describe('logServerStart', () => {
it('logs flag usage', async () => {
const logger = new ClearcutLogger({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
await logger.logServerStart({headless: true});
assert(mockWatchdogClient.send.calledOnce);
const msg = mockWatchdogClient.send.firstCall.args[0];
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
assert.strictEqual(msg.payload.server_start?.flag_usage?.headless, true);
});
});
describe('logDailyActiveIfNeeded', () => {
it('logs daily active if needed (lastActive > 24h ago)', async () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
mockPersistence.loadState.resolves({
lastActive: yesterday.toISOString(),
});
const logger = new ClearcutLogger({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
await logger.logDailyActiveIfNeeded();
assert(mockWatchdogClient.send.calledOnce);
const msg = mockWatchdogClient.send.firstCall.args[0];
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
assert.ok(msg.payload.daily_active);
assert(mockPersistence.saveState.called);
});
it('does not log daily active if not needed (today)', async () => {
mockPersistence.loadState.resolves({
lastActive: new Date().toISOString(),
});
const logger = new ClearcutLogger({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
await logger.logDailyActiveIfNeeded();
assert(mockWatchdogClient.send.notCalled);
assert(mockPersistence.saveState.notCalled);
});
it('logs daily active with -1 if lastActive is missing', async () => {
mockPersistence.loadState.resolves({
lastActive: '',
});
const logger = new ClearcutLogger({
persistence: mockPersistence,
appVersion: '1.0.0',
watchdogClient: mockWatchdogClient,
});
await logger.logDailyActiveIfNeeded();
assert(mockWatchdogClient.send.calledOnce);
const msg = mockWatchdogClient.send.firstCall.args[0];
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
assert.strictEqual(msg.payload.daily_active?.days_since_last_active, -1);
assert(mockPersistence.saveState.called);
});
});
});