Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 78 additions & 21 deletions src/telemetry/ClearcutLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ function detectOsType(): OsType {
}
}

function isSameDay(d1: Date, d2: Date): boolean {
return (
d1.getUTCFullYear() === d2.getUTCFullYear() &&
d1.getUTCMonth() === d2.getUTCMonth() &&
d1.getUTCDate() === d2.getUTCDate()
);
}

function shouldLogDailyActive(state: LocalState): boolean {
if (!state.lastActive) {
return true;
}
return !isSameDay(new Date(state.lastActive), new Date());
}

function calculateDaysSince(
lastDateString?: string,
now: Date = new Date(),
): number {
if (!lastDateString) {
return -1;
}
const lastDate = new Date(lastDateString);
const diffTime = Math.abs(now.getTime() - lastDate.getTime());
return Math.ceil(diffTime / MS_PER_DAY);
}

export interface ClearcutLoggerOptions {
appVersion: string;
persistence: Persistence;
Expand All @@ -61,6 +88,7 @@ export class ClearcutLogger {
#persistence: Persistence;
#watchdog: WatchdogClient;
#mcpClient: McpClient;
#state?: LocalState;

static initialize(options: ClearcutLoggerOptions): ClearcutLogger {
if (_clearcut_logger_instance) {
Expand Down Expand Up @@ -92,6 +120,15 @@ export class ClearcutLogger {
clearcutIncludePidHeader: options.clearcutIncludePidHeader,
});
this.#mcpClient = McpClient.MCP_CLIENT_UNSPECIFIED;
void this.#persistence
.loadState()
.then(state => {
this.#state = state;
Comment thread
yulunz marked this conversation as resolved.
})
.catch(error => {
this.#state = undefined;
logger?.('Failed to load telemetry state:', error);
});
}

setClientName(clientName: string): void {
Expand Down Expand Up @@ -132,6 +169,10 @@ export class ClearcutLogger {
devToolsData?: DevToolsData;
pageUrl?: string;
}): Promise<void> {
void this.#logToolActiveIfNeeded().catch(error => {
logger?.('Error in logToolActiveIfNeeded:', error);
});

const context = buildContext(args.devToolsData, args.pageUrl);
const sanitizedToolName = stripUnderscoreBeforeNumber(args.toolName);
const tool_invocation: ToolInvocation = {
Expand Down Expand Up @@ -174,16 +215,10 @@ export class ClearcutLogger {

async logDailyActiveIfNeeded(): Promise<void> {
try {
const state = await this.#persistence.loadState();
this.#state = await this.#persistence.loadState();

if (this.#shouldLogDailyActive(state)) {
let daysSince = -1;
if (state.lastActive) {
const lastActiveDate = new Date(state.lastActive);
const now = new Date();
const diffTime = Math.abs(now.getTime() - lastActiveDate.getTime());
daysSince = Math.ceil(diffTime / MS_PER_DAY);
}
if (shouldLogDailyActive(this.#state)) {
const daysSince = calculateDaysSince(this.#state.lastActive);

this.#watchdog.send({
type: WatchdogMessageType.LOG_EVENT,
Expand All @@ -195,8 +230,8 @@ export class ClearcutLogger {
},
});

state.lastActive = new Date().toISOString();
await this.#persistence.saveState(state);
this.#state.lastActive = new Date().toISOString();
await this.#persistence.saveState(this.#state);
}
} catch (err) {
logger?.('Error in logDailyActiveIfNeeded:', err);
Expand All @@ -221,19 +256,41 @@ export class ClearcutLogger {
});
}

#shouldLogDailyActive(state: LocalState): boolean {
if (!state.lastActive) {
return true;
async #logToolActiveIfNeeded(): Promise<void> {
// Expect state loaded at first tool call, if not, just skip logging.
if (!this.#state) {
return;
}
const lastActiveDate = new Date(state.lastActive);

// Don't log tool active if it has already been logged today.
const now = new Date();
if (
this.#state.lastToolCall &&
isSameDay(now, new Date(this.#state.lastToolCall))
) {
return;
}

// Refresh state in case it now contains more recent value, and test again.
const state = await this.#persistence.loadState();
this.#state = state;
if (state.lastToolCall && isSameDay(now, new Date(state.lastToolCall))) {
return;
}

// Compare UTC dates
const isSameDay =
lastActiveDate.getUTCFullYear() === now.getUTCFullYear() &&
lastActiveDate.getUTCMonth() === now.getUTCMonth() &&
lastActiveDate.getUTCDate() === now.getUTCDate();
const daysSinceToolCall = calculateDaysSince(state.lastToolCall, now);

this.#watchdog.send({
type: WatchdogMessageType.LOG_EVENT,
payload: {
mcp_client: this.#mcpClient,
tool_active: {
days_since_last_tool_call: bucketizeDaysSince(daysSinceToolCall),
},
},
});

return !isSameDay;
this.#state.lastToolCall = now.toISOString();
await this.#persistence.saveState(this.#state);
}
}
30 changes: 22 additions & 8 deletions src/telemetry/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,22 @@ import {ClearcutLogger} from './ClearcutLogger.js';
import {ErrorCode} from './errors.js';

export interface LocalState {
lastActive: string; // ISO 8601 UTC date string
lastActive?: string; // ISO 8601 UTC date string
lastToolCall?: string; // ISO 8601 UTC date string
}

function isValidTime(time?: unknown): boolean {
if (time === undefined) {
return true;
}
if (typeof time !== 'string' || time === '') {
return false;
}
return !Number.isNaN(new Date(time).getTime());
}

function isContextValid(state: LocalState): boolean {
return isValidTime(state.lastActive) && isValidTime(state.lastToolCall);
}

const STATE_FILE_NAME = 'telemetry_state.json';
Expand Down Expand Up @@ -58,23 +73,22 @@ export class FilePersistence implements Persistence {
await fs.access(filePath);
} catch {
// File doesn't exist. Not an error because new users do not have the state file.
return {
lastActive: '',
};
return {};
}

let state;
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content) as LocalState;
state = JSON.parse(content) as LocalState;
} catch (error) {
logger?.(`Failed to read telemetry state from ${filePath}:`, error);
void ClearcutLogger.get()?.logServerError({
errorCode: ErrorCode.ERROR_CODE_PERSISTENCE_FILE_READ_FAILED,
});
return {
lastActive: '',
};
return {};
}

return isContextValid(state) ? state : {};
}

async saveState(state: LocalState): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ChromeDevToolsMcpExtension {
daily_active?: DailyActive;
server_shutdown?: ServerShutdown;
server_error?: ServerError;
tool_active?: ToolActive;
}

export interface ServerError {
Expand Down Expand Up @@ -53,6 +54,10 @@ export interface DailyActive {
days_since_last_active: number;
}

export interface ToolActive {
days_since_last_tool_call: number;
}

export type FlagUsage = Record<string, boolean | string | number | undefined>;

// Clearcut API interfaces
Expand Down
Loading