Skip to content

Commit a1befc1

Browse files
committed
[Telemetry] Implement Phase 3: Main Process Integration
1 parent 50dc7ac commit a1befc1

5 files changed

Lines changed: 133 additions & 22 deletions

File tree

src/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export const args = parseArguments(VERSION);
3838
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
3939
let clearcutLogger: ClearcutLogger | undefined;
4040
if (args.usageStatistics) {
41-
clearcutLogger = new ClearcutLogger();
41+
clearcutLogger = new ClearcutLogger({
42+
logFile: args.logFile,
43+
appVersion: VERSION,
44+
});
4245
}
4346

4447
process.on('unhandledRejection', (reason, promise) => {

src/telemetry/clearcut-logger.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,72 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import process from 'node:process';
8+
79
import {logger} from '../logger.js';
810

9-
import {ClearcutSender} from './clearcut-sender.js';
1011
import type {LocalState, Persistence} from './persistence.js';
1112
import {FilePersistence} from './persistence.js';
12-
import type {FlagUsage} from './types.js';
13+
import {type FlagUsage, IpcMessageType, OsType} from './types.js';
14+
import {WatchdogClient} from './watchdog-client.js';
1315

1416
const MS_PER_DAY = 24 * 60 * 60 * 1000;
1517

1618
export class ClearcutLogger {
1719
#persistence: Persistence;
18-
#sender: ClearcutSender;
20+
#watchdog: WatchdogClient;
1921

20-
constructor(options?: {persistence?: Persistence; sender?: ClearcutSender}) {
21-
this.#persistence = options?.persistence ?? new FilePersistence();
22-
this.#sender = options?.sender ?? new ClearcutSender();
22+
constructor(options: {
23+
persistence?: Persistence;
24+
logFile?: string;
25+
appVersion: string;
26+
}) {
27+
this.#persistence = options.persistence ?? new FilePersistence();
28+
this.#watchdog = new WatchdogClient({
29+
parentPid: process.pid,
30+
appVersion: options.appVersion,
31+
osType: this.#detectOsType(),
32+
logFile: options.logFile,
33+
});
34+
}
35+
36+
#detectOsType(): OsType {
37+
switch (process.platform) {
38+
case 'win32':
39+
return OsType.OS_TYPE_WINDOWS;
40+
case 'darwin':
41+
return OsType.OS_TYPE_MACOS;
42+
case 'linux':
43+
return OsType.OS_TYPE_LINUX;
44+
default:
45+
return OsType.OS_TYPE_UNSPECIFIED;
46+
}
2347
}
2448

2549
async logToolInvocation(args: {
2650
toolName: string;
2751
success: boolean;
2852
latencyMs: number;
2953
}): Promise<void> {
30-
await this.#sender.send({
31-
tool_invocation: {
32-
tool_name: args.toolName,
33-
success: args.success,
34-
latency_ms: args.latencyMs,
54+
this.#watchdog.send({
55+
type: IpcMessageType.EVENT,
56+
payload: {
57+
tool_invocation: {
58+
tool_name: args.toolName,
59+
success: args.success,
60+
latency_ms: args.latencyMs,
61+
},
3562
},
3663
});
3764
}
3865

3966
async logServerStart(flagUsage: FlagUsage): Promise<void> {
40-
await this.#sender.send({
41-
server_start: {
42-
flag_usage: flagUsage,
67+
this.#watchdog.send({
68+
type: IpcMessageType.EVENT,
69+
payload: {
70+
server_start: {
71+
flag_usage: flagUsage,
72+
},
4373
},
4474
});
4575
}
@@ -57,9 +87,12 @@ export class ClearcutLogger {
5787
daysSince = Math.ceil(diffTime / MS_PER_DAY);
5888
}
5989

60-
await this.#sender.send({
61-
daily_active: {
62-
days_since_last_active: daysSince,
90+
this.#watchdog.send({
91+
type: IpcMessageType.EVENT,
92+
payload: {
93+
daily_active: {
94+
days_since_last_active: daysSince,
95+
},
6396
},
6497
});
6598

src/telemetry/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ export interface LogRequest {
4949

5050
// IPC Types
5151
export enum IpcMessageType {
52-
DATA = 'data',
52+
EVENT = 'event',
5353
}
5454

55-
export interface TelemetryEvent {
56-
type: IpcMessageType.DATA;
55+
export interface IpcMessage {
56+
type: IpcMessageType.EVENT;
5757
payload: ChromeDevToolsMcpExtension;
5858
}
5959

src/telemetry/watchdog-client.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {spawn, type ChildProcess} from 'node:child_process';
8+
import {fileURLToPath} from 'node:url';
9+
10+
import {logger} from '../logger.js';
11+
12+
import type {IpcMessage, OsType} from './types.js';
13+
14+
export class WatchdogClient {
15+
#childProcess: ChildProcess;
16+
17+
constructor(
18+
config: {
19+
parentPid: number;
20+
appVersion: string;
21+
osType: OsType;
22+
logFile?: string;
23+
},
24+
options?: {spawn?: typeof spawn},
25+
) {
26+
const watchdogPath = fileURLToPath(
27+
new URL('./watchdog/main.js', import.meta.url),
28+
);
29+
30+
const args = [
31+
watchdogPath,
32+
`--parent-pid=${config.parentPid}`,
33+
`--app-version=${config.appVersion}`,
34+
`--os-type=${config.osType}`,
35+
];
36+
37+
if (config.logFile) {
38+
args.push(`--log-file=${config.logFile}`);
39+
}
40+
41+
const spawner = options?.spawn ?? spawn;
42+
this.#childProcess = spawner(process.execPath, args, {
43+
stdio: ['pipe', 'ignore', 'ignore'],
44+
detached: false, // Keep attached to parent lifecycle generally, but unref to not block exit
45+
});
46+
47+
// Don't let the watchdog keep the main process alive
48+
this.#childProcess.unref();
49+
50+
this.#childProcess.on('error', (err) => {
51+
logger('Watchdog process error:', err);
52+
});
53+
54+
this.#childProcess.on('exit', (code, signal) => {
55+
logger(`Watchdog exited with code ${code} and signal ${signal}`);
56+
});
57+
}
58+
59+
send(message: IpcMessage): void {
60+
if (
61+
this.#childProcess.stdin &&
62+
!this.#childProcess.stdin.destroyed &&
63+
this.#childProcess.pid
64+
) {
65+
try {
66+
const line = JSON.stringify(message) + '\n';
67+
this.#childProcess.stdin.write(line);
68+
} catch (err) {
69+
logger('Failed to write to watchdog stdin', err);
70+
}
71+
} else {
72+
logger('Watchdog stdin not available, dropping message');
73+
}
74+
}
75+
}

src/telemetry/watchdog/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async function main() {
8989
if (!line.trim()) return;
9090

9191
const msg = JSON.parse(line);
92-
if (msg.type === IpcMessageType.DATA && msg.payload) {
92+
if (msg.type === IpcMessageType.EVENT && msg.payload) {
9393
sender.send(msg.payload).catch((err) => {
9494
logger('Error sending event', err);
9595
});

0 commit comments

Comments
 (0)