Fix telemetry shutdown noise and improve token usage diagnostics (#444)

Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-03-13 13:57:49 +08:00
committed by GitHub
Unverified
parent 01adc828b5
commit 995a7f070d
21 changed files with 923 additions and 116 deletions

View File

@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IncomingMessage, ServerResponse } from 'http';
const runOpenClawDoctorMock = vi.fn();
const runOpenClawDoctorFixMock = vi.fn();
const sendJsonMock = vi.fn();
const sendNoContentMock = vi.fn();
vi.mock('@electron/utils/openclaw-doctor', () => ({
runOpenClawDoctor: (...args: unknown[]) => runOpenClawDoctorMock(...args),
runOpenClawDoctorFix: (...args: unknown[]) => runOpenClawDoctorFixMock(...args),
}));
vi.mock('@electron/api/route-utils', () => ({
setCorsHeaders: vi.fn(),
parseJsonBody: vi.fn().mockResolvedValue({}),
sendJson: (...args: unknown[]) => sendJsonMock(...args),
sendNoContent: (...args: unknown[]) => sendNoContentMock(...args),
}));
describe('handleAppRoutes', () => {
beforeEach(() => {
vi.resetAllMocks();
});
it('runs openclaw doctor through the host api', async () => {
runOpenClawDoctorMock.mockResolvedValueOnce({ success: true, exitCode: 0 });
const { handleAppRoutes } = await import('@electron/api/routes/app');
const handled = await handleAppRoutes(
{ method: 'POST' } as IncomingMessage,
{} as ServerResponse,
new URL('http://127.0.0.1:3210/api/app/openclaw-doctor'),
{} as never,
);
expect(handled).toBe(true);
expect(runOpenClawDoctorMock).toHaveBeenCalledTimes(1);
expect(sendJsonMock).toHaveBeenCalledWith(expect.anything(), 200, { success: true, exitCode: 0 });
});
it('runs openclaw doctor fix when requested', async () => {
const { parseJsonBody } = await import('@electron/api/route-utils');
vi.mocked(parseJsonBody).mockResolvedValueOnce({ mode: 'fix' });
runOpenClawDoctorFixMock.mockResolvedValueOnce({ success: false, exitCode: 1 });
const { handleAppRoutes } = await import('@electron/api/routes/app');
const handled = await handleAppRoutes(
{ method: 'POST' } as IncomingMessage,
{} as ServerResponse,
new URL('http://127.0.0.1:3210/api/app/openclaw-doctor'),
{} as never,
);
expect(handled).toBe(true);
expect(runOpenClawDoctorFixMock).toHaveBeenCalledTimes(1);
expect(sendJsonMock).toHaveBeenCalledWith(expect.anything(), 200, { success: false, exitCode: 1 });
});
});

View File

@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest';
import {
filterUsageHistoryByWindow,
groupUsageHistory,
type UsageHistoryEntry,
} from '@/pages/Models/usage-history';
function createEntry(day: number, totalTokens: number): UsageHistoryEntry {
return {
timestamp: `2026-03-${String(day).padStart(2, '0')}T12:00:00.000Z`,
sessionId: `session-${day}`,
agentId: 'main',
model: 'gpt-5',
inputTokens: totalTokens,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalTokens,
};
}
describe('models usage history helpers', () => {
it('keeps all day buckets instead of truncating to the first eight', () => {
const entries = Array.from({ length: 12 }, (_, index) => createEntry(index + 1, index + 1));
const groups = groupUsageHistory(entries, 'day');
expect(groups).toHaveLength(12);
expect(groups[0]?.totalTokens).toBe(1);
expect(groups[11]?.totalTokens).toBe(12);
});
it('limits model buckets to the top eight by total tokens', () => {
const entries = Array.from({ length: 10 }, (_, index) => ({
...createEntry(index + 1, index + 1),
model: `model-${index + 1}`,
}));
const groups = groupUsageHistory(entries, 'model');
expect(groups).toHaveLength(8);
expect(groups[0]?.label).toBe('model-10');
expect(groups[7]?.label).toBe('model-3');
});
it('filters the last 30 days relative to now instead of calendar month boundaries', () => {
const now = Date.parse('2026-03-12T12:00:00.000Z');
const entries = [
{
...createEntry(12, 12),
timestamp: '2026-03-12T12:00:00.000Z',
},
{
...createEntry(11, 11),
timestamp: '2026-02-11T12:00:00.000Z',
},
{
...createEntry(10, 10),
timestamp: '2026-02-10T11:59:59.000Z',
},
];
const filtered = filterUsageHistoryByWindow(entries, '30d', now);
expect(filtered).toHaveLength(2);
expect(filtered.map((entry) => entry.totalTokens)).toEqual([12, 11]);
});
});

View File

@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const {
shutdownMock,
captureMock,
getSettingMock,
setSettingMock,
loggerDebugMock,
loggerErrorMock,
} = vi.hoisted(() => ({
shutdownMock: vi.fn(),
captureMock: vi.fn(),
getSettingMock: vi.fn(),
setSettingMock: vi.fn(),
loggerDebugMock: vi.fn(),
loggerErrorMock: vi.fn(),
}));
vi.mock('posthog-node', () => ({
PostHog: vi.fn(function PostHogMock() {
return {
capture: captureMock,
shutdown: shutdownMock,
};
}),
}));
vi.mock('@electron/utils/store', () => ({
getSetting: getSettingMock,
setSetting: setSettingMock,
}));
vi.mock('@electron/utils/logger', () => ({
logger: {
debug: loggerDebugMock,
error: loggerErrorMock,
info: vi.fn(),
warn: vi.fn(),
},
}));
vi.mock('electron', () => ({
app: {
getVersion: () => '0.2.1',
},
}));
vi.mock('node-machine-id', () => ({
machineIdSync: () => 'machine-id-1',
}));
describe('main telemetry shutdown', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
getSettingMock.mockImplementation(async (key: string) => {
switch (key) {
case 'telemetryEnabled':
return true;
case 'machineId':
return 'existing-machine-id';
case 'hasReportedInstall':
return true;
default:
return undefined;
}
});
setSettingMock.mockResolvedValue(undefined);
captureMock.mockReturnValue(undefined);
});
it('ignores PostHog network timeout errors during shutdown', async () => {
shutdownMock.mockRejectedValueOnce(
Object.assign(new Error('Network error while fetching PostHog'), {
name: 'PostHogFetchNetworkError',
cause: Object.assign(new Error('The operation was aborted due to timeout'), {
name: 'TimeoutError',
}),
}),
);
const { initTelemetry, shutdownTelemetry } = await import('@electron/utils/telemetry');
await initTelemetry();
await shutdownTelemetry();
expect(loggerErrorMock).not.toHaveBeenCalled();
expect(loggerDebugMock).toHaveBeenCalledWith(
'Ignored telemetry shutdown network error:',
expect.objectContaining({ name: 'PostHogFetchNetworkError' }),
);
});
});

View File

@@ -14,6 +14,10 @@ describe('extractSessionIdFromTranscriptFileName', () => {
expect(extractSessionIdFromTranscriptFileName('abc-123.jsonl.reset.2026-03-09T03-01-29.968Z')).toBe('abc-123');
});
it('parses deleted reset transcript names', () => {
expect(extractSessionIdFromTranscriptFileName('abc-123.deleted.jsonl.reset.2026-03-09T03-01-29.968Z')).toBe('abc-123');
});
it('returns undefined for non-transcript files', () => {
expect(extractSessionIdFromTranscriptFileName('sessions.json')).toBeUndefined();
expect(extractSessionIdFromTranscriptFileName('abc-123.log')).toBeUndefined();

View File

@@ -0,0 +1,89 @@
import { mkdir, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { testHome, testUserData } = vi.hoisted(() => {
const suffix = Math.random().toString(36).slice(2);
return {
testHome: `/tmp/clawx-token-usage-${suffix}`,
testUserData: `/tmp/clawx-token-usage-user-data-${suffix}`,
};
});
vi.mock('os', async () => {
const actual = await vi.importActual<typeof import('os')>('os');
const mocked = {
...actual,
homedir: () => testHome,
};
return {
...mocked,
default: mocked,
};
});
vi.mock('electron', () => ({
app: {
isPackaged: false,
getPath: () => testUserData,
getVersion: () => '0.0.0-test',
},
}));
describe('token usage session scan', () => {
beforeEach(async () => {
vi.resetModules();
vi.restoreAllMocks();
await rm(testHome, { recursive: true, force: true });
await rm(testUserData, { recursive: true, force: true });
});
it('includes transcripts from agent directories that exist on disk but are not configured', async () => {
const openclawDir = join(testHome, '.openclaw');
await mkdir(openclawDir, { recursive: true });
await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify({
agents: {
list: [
{ id: 'main', name: 'Main', default: true },
],
},
}, null, 2), 'utf8');
const diskOnlySessionsDir = join(openclawDir, 'agents', 'custom-custom25', 'sessions');
await mkdir(diskOnlySessionsDir, { recursive: true });
await writeFile(
join(diskOnlySessionsDir, 'f8e66f77-0125-4e2f-b750-9c4de01e8f5a.jsonl'),
[
JSON.stringify({
type: 'message',
timestamp: '2026-03-12T12:19:00.000Z',
message: {
role: 'assistant',
model: 'gpt-5.2-2025-12-11',
provider: 'openai',
usage: {
input: 17649,
output: 107,
total: 17756,
},
},
}),
].join('\n'),
'utf8',
);
const { getRecentTokenUsageHistory } = await import('@electron/utils/token-usage');
const entries = await getRecentTokenUsageHistory();
expect(entries).toEqual(
expect.arrayContaining([
expect.objectContaining({
agentId: 'custom-custom25',
sessionId: 'f8e66f77-0125-4e2f-b750-9c4de01e8f5a',
model: 'gpt-5.2-2025-12-11',
totalTokens: 17756,
}),
]),
);
});
});