feat: add diagnostic log ring buffer

这个提交包含在:
Zain 2026-03-19 18:36:39 +08:00
父节点 bf7bc7e6bb
当前提交 fb2b028f91
共有 3 个文件被更改,包括 151 次插入 和 0 次删除

查看文件

@ -0,0 +1,33 @@
import { type DiagnosticLogEntry, DIAGNOSTIC_STORAGE_KEYS } from '../types/diagnostic';
import { type StorageManager, storage } from '../utils/storage';
const MAX_RECENT_LOGS = 300;
type DiagnosticStoreStorage = Pick<StorageManager, 'get' | 'set' | 'remove'>;
export class DiagnosticLogStore {
constructor(private readonly storageManager: DiagnosticStoreStorage = storage) {}
async append(entry: DiagnosticLogEntry): Promise<void> {
const recent = await this.getRecent();
const next = [...recent, entry];
if (next.length > MAX_RECENT_LOGS) {
next.splice(0, next.length - MAX_RECENT_LOGS);
}
await this.storageManager.set(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS, next);
}
async getRecent(): Promise<DiagnosticLogEntry[]> {
const stored = await this.storageManager.get<DiagnosticLogEntry[]>(
DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS
);
return Array.isArray(stored) ? stored : [];
}
async clear(): Promise<void> {
await this.storageManager.remove(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS);
}
}

54
src/types/diagnostic.ts 普通文件
查看文件

@ -0,0 +1,54 @@
/**
* 诊断日志级别
*/
export type DiagnosticLogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
/**
* 单条诊断日志
*/
export interface DiagnosticLogEntry {
id: string;
level: DiagnosticLogLevel;
message: string;
timestamp?: number;
context?: Record<string, unknown>;
}
/**
* 单次失败记录
*/
export interface DiagnosticFailureEntry {
id: string;
occurredAt: number;
reason: string;
message?: string;
}
/**
* 最近一次失败的诊断快照
*/
export interface LastFailureDiagnosticSnapshot {
failure: DiagnosticFailureEntry;
recentLogs: DiagnosticLogEntry[];
}
/**
* 导出诊断载荷
*/
export interface DiagnosticExportPayload {
exportedAt: number;
lastFailure: LastFailureDiagnosticSnapshot | null;
}
/**
* 诊断提示状态
*/
export interface DiagnosticPromptState {
shouldPrompt: boolean;
lastPromptedAt?: number;
}
export const DIAGNOSTIC_STORAGE_KEYS = {
RECENT_LOGS: 'diagnostic_recent_logs',
LAST_FAILURE: 'diagnostic_last_failure',
} as const;

查看文件

@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { StorageManager } from '../../src/utils/storage';
import { DiagnosticLogStore } from '../../src/background/diagnostic-log-store';
import { DIAGNOSTIC_STORAGE_KEYS, type DiagnosticLogEntry } from '../../src/types/diagnostic';
describe('DiagnosticLogStore', () => {
let mockChromeStorage: {
get: ReturnType<typeof vi.fn>;
set: ReturnType<typeof vi.fn>;
remove: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
mockChromeStorage = {
get: vi.fn(() => Promise.resolve({})),
set: vi.fn(() => Promise.resolve()),
remove: vi.fn(() => Promise.resolve()),
};
global.chrome = {
storage: {
local: mockChromeStorage,
},
} as any;
global.localStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
key: vi.fn(),
length: 0,
} as any;
});
it('keeps only the latest 300 diagnostic log entries', async () => {
let currentLogs: DiagnosticLogEntry[] = [];
mockChromeStorage.get.mockImplementation(async () => ({
[DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS]: currentLogs,
}));
mockChromeStorage.set.mockImplementation(async (payload: Record<string, DiagnosticLogEntry[]>) => {
currentLogs = payload[DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS] ?? [];
});
const store = new DiagnosticLogStore(new StorageManager());
for (let i = 0; i < 305; i++) {
await store.append({ id: `log-${i}`, level: 'INFO', message: `m-${i}` } as DiagnosticLogEntry);
}
const logs = await store.getRecent();
expect(logs).toHaveLength(300);
expect(logs[0].id).toBe('log-5');
expect(logs.at(-1)?.id).toBe('log-304');
});
it('clears the buffer without touching unrelated keys', async () => {
const store = new DiagnosticLogStore(new StorageManager());
await store.clear();
expect(mockChromeStorage.remove).toHaveBeenCalledWith(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS);
});
});