Skip to content

Commit a0d6ea1

Browse files
authored
chore: make Clearcut logger a global singleton (#1999)
We will add a error logging method to ClearcutLogger in a follow-up PR. Since the error can happen anywhere in the stack, the logger instance has to be readily available (i.e. w/o passing the logger instance everywhere). This commit registers the instantiated logger as a global singleton, and makes it possible to retrieve it by a static method (`ClearcutLogger.get()`) wherever we need it.
1 parent d842de1 commit a0d6ea1

4 files changed

Lines changed: 89 additions & 24 deletions

File tree

src/bin/chrome-devtools-mcp-main.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import process from 'node:process';
1010

1111
import {createMcpServer, logDisclaimers} from '../index.js';
1212
import {logger, saveLogsToFile} from '../logger.js';
13+
import {ClearcutLogger} from '../telemetry/ClearcutLogger.js';
1314
import {computeFlagUsage} from '../telemetry/flagUtils.js';
1415
import {StdioServerTransport} from '../third_party/index.js';
1516
import {checkForUpdates} from '../utils/check-for-updates.js';
@@ -32,12 +33,12 @@ if (process.env['CHROME_DEVTOOLS_MCP_CRASH_ON_UNCAUGHT'] !== 'true') {
3233
}
3334

3435
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
35-
const {server, clearcutLogger} = await createMcpServer(args, {
36+
const {server} = await createMcpServer(args, {
3637
logFile,
3738
});
3839
const transport = new StdioServerTransport();
3940
await server.connect(transport);
4041
logger('Chrome DevTools MCP Server connected');
4142
logDisclaimers(args);
42-
void clearcutLogger?.logDailyActiveIfNeeded();
43-
void clearcutLogger?.logServerStart(computeFlagUsage(args, cliOptions));
43+
void ClearcutLogger.get()?.logDailyActiveIfNeeded();
44+
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, cliOptions));

src/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,8 @@ export async function createMcpServer(
134134
logFile?: fs.WriteStream;
135135
},
136136
) {
137-
let clearcutLogger: ClearcutLogger | undefined;
138137
if (serverArgs.usageStatistics) {
139-
clearcutLogger = new ClearcutLogger({
138+
ClearcutLogger.initialize({
140139
logFile: serverArgs.logFile,
141140
appVersion: VERSION,
142141
clearcutEndpoint: serverArgs.clearcutEndpoint,
@@ -175,7 +174,7 @@ export async function createMcpServer(
175174
server.server.oninitialized = () => {
176175
const clientName = server.server.getClientVersion()?.name;
177176
if (clientName) {
178-
clearcutLogger?.setClientName(clientName);
177+
ClearcutLogger.get()?.setClientName(clientName);
179178
}
180179
if (server.server.getClientCapabilities()?.roots) {
181180
void updateRoots();
@@ -360,7 +359,7 @@ export async function createMcpServer(
360359
isError: true,
361360
};
362361
} finally {
363-
void clearcutLogger?.logToolInvocation({
362+
void ClearcutLogger.get()?.logToolInvocation({
364363
toolName: tool.name,
365364
params,
366365
schema,
@@ -380,7 +379,7 @@ export async function createMcpServer(
380379

381380
await loadIssueDescriptions();
382381

383-
return {server, clearcutLogger};
382+
return {server};
384383
}
385384

386385
export const logDisclaimers = (args: ReturnType<typeof parseArguments>) => {

src/telemetry/ClearcutLogger.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,41 @@ function detectOsType(): OsType {
171171
}
172172
}
173173

174+
export interface ClearcutLoggerOptions {
175+
appVersion: string;
176+
logFile?: string;
177+
persistence?: Persistence;
178+
watchdogClient?: WatchdogClient;
179+
clearcutEndpoint?: string;
180+
clearcutForceFlushIntervalMs?: number;
181+
clearcutIncludePidHeader?: boolean;
182+
}
183+
184+
// Not const to allow resetting the instance for testing purposes.
185+
let _clearcut_logger_instance: ClearcutLogger | undefined;
186+
174187
export class ClearcutLogger {
175188
#persistence: Persistence;
176189
#watchdog: WatchdogClient;
177190
#mcpClient: McpClient;
178191

179-
constructor(options: {
180-
appVersion: string;
181-
logFile?: string;
182-
persistence?: Persistence;
183-
watchdogClient?: WatchdogClient;
184-
clearcutEndpoint?: string;
185-
clearcutForceFlushIntervalMs?: number;
186-
clearcutIncludePidHeader?: boolean;
187-
}) {
192+
static initialize(options: ClearcutLoggerOptions): ClearcutLogger {
193+
if (_clearcut_logger_instance) {
194+
throw new Error('ClearcutLogger is already initialized');
195+
}
196+
_clearcut_logger_instance = new ClearcutLogger(options);
197+
return _clearcut_logger_instance;
198+
}
199+
200+
static get(): ClearcutLogger | undefined {
201+
return _clearcut_logger_instance;
202+
}
203+
204+
static resetForTesting(): void {
205+
_clearcut_logger_instance = undefined;
206+
}
207+
208+
private constructor(options: ClearcutLoggerOptions) {
188209
this.#persistence = options.persistence ?? new FilePersistence();
189210
this.#watchdog =
190211
options.watchdogClient ??

tests/telemetry/ClearcutLogger.test.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('ClearcutLogger', () => {
2525
let mockWatchdogClient: sinon.SinonStubbedInstance<WatchdogClient>;
2626

2727
beforeEach(() => {
28+
ClearcutLogger.resetForTesting();
2829
mockPersistence = sinon.createStubInstance(FilePersistence, {
2930
loadState: Promise.resolve({
3031
lastActive: '',
@@ -35,11 +36,12 @@ describe('ClearcutLogger', () => {
3536

3637
afterEach(() => {
3738
sinon.restore();
39+
ClearcutLogger.resetForTesting();
3840
});
3941

4042
describe('logToolInvocation', () => {
4143
it('sends correct payload', async () => {
42-
const logger = new ClearcutLogger({
44+
const logger = ClearcutLogger.initialize({
4345
persistence: mockPersistence,
4446
appVersion: '1.0.0',
4547
watchdogClient: mockWatchdogClient,
@@ -60,7 +62,7 @@ describe('ClearcutLogger', () => {
6062
assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
6163
});
6264
it('sends sanitized params', async () => {
63-
const logger = new ClearcutLogger({
65+
const logger = ClearcutLogger.initialize({
6466
persistence: mockPersistence,
6567
appVersion: '1.0.0',
6668
watchdogClient: mockWatchdogClient,
@@ -107,7 +109,7 @@ describe('ClearcutLogger', () => {
107109

108110
for (const {name, expected} of clients) {
109111
it(`maps ${name} client correctly`, async () => {
110-
const logger = new ClearcutLogger({
112+
const logger = ClearcutLogger.initialize({
111113
persistence: mockPersistence,
112114
appVersion: '1.0.0',
113115
watchdogClient: mockWatchdogClient,
@@ -126,7 +128,7 @@ describe('ClearcutLogger', () => {
126128

127129
describe('logServerStart', () => {
128130
it('logs flag usage', async () => {
129-
const logger = new ClearcutLogger({
131+
const logger = ClearcutLogger.initialize({
130132
persistence: mockPersistence,
131133
appVersion: '1.0.0',
132134
watchdogClient: mockWatchdogClient,
@@ -150,7 +152,7 @@ describe('ClearcutLogger', () => {
150152
lastActive: yesterday.toISOString(),
151153
});
152154

153-
const logger = new ClearcutLogger({
155+
const logger = ClearcutLogger.initialize({
154156
persistence: mockPersistence,
155157
appVersion: '1.0.0',
156158
watchdogClient: mockWatchdogClient,
@@ -171,7 +173,7 @@ describe('ClearcutLogger', () => {
171173
lastActive: new Date().toISOString(),
172174
});
173175

174-
const logger = new ClearcutLogger({
176+
const logger = ClearcutLogger.initialize({
175177
persistence: mockPersistence,
176178
appVersion: '1.0.0',
177179
watchdogClient: mockWatchdogClient,
@@ -188,7 +190,7 @@ describe('ClearcutLogger', () => {
188190
lastActive: '',
189191
});
190192

191-
const logger = new ClearcutLogger({
193+
const logger = ClearcutLogger.initialize({
192194
persistence: mockPersistence,
193195
appVersion: '1.0.0',
194196
watchdogClient: mockWatchdogClient,
@@ -292,4 +294,46 @@ describe('ClearcutLogger', () => {
292294
);
293295
});
294296
});
297+
298+
describe('Singleton', () => {
299+
it('returns undefined if not initialized', () => {
300+
assert.strictEqual(ClearcutLogger.get(), undefined);
301+
});
302+
303+
it('returns instance after initialization', () => {
304+
const logger = ClearcutLogger.initialize({
305+
persistence: mockPersistence,
306+
appVersion: '1.0.0',
307+
watchdogClient: mockWatchdogClient,
308+
});
309+
assert.strictEqual(ClearcutLogger.get(), logger);
310+
});
311+
312+
it('throws error if initialized twice', () => {
313+
ClearcutLogger.initialize({
314+
persistence: mockPersistence,
315+
appVersion: '1.0.0',
316+
watchdogClient: mockWatchdogClient,
317+
});
318+
319+
assert.throws(() => {
320+
ClearcutLogger.initialize({
321+
persistence: mockPersistence,
322+
appVersion: '1.0.0',
323+
watchdogClient: mockWatchdogClient,
324+
});
325+
}, /ClearcutLogger is already initialized/);
326+
});
327+
328+
it('resets instance for testing', () => {
329+
ClearcutLogger.initialize({
330+
persistence: mockPersistence,
331+
appVersion: '1.0.0',
332+
watchdogClient: mockWatchdogClient,
333+
});
334+
335+
ClearcutLogger.resetForTesting();
336+
assert.strictEqual(ClearcutLogger.get(), undefined);
337+
});
338+
});
295339
});

0 commit comments

Comments
 (0)