(options?: {
collectRemote?: boolean
})
| 2794 | // ============================================================================ |
| 2795 | |
| 2796 | export async function generateUsageReport(options?: { |
| 2797 | collectRemote?: boolean |
| 2798 | }): Promise<{ |
| 2799 | insights: InsightResults |
| 2800 | htmlPath: string |
| 2801 | data: AggregatedData |
| 2802 | remoteStats?: { hosts: RemoteHostInfo[]; totalCopied: number } |
| 2803 | facets: Map<string, SessionFacets> |
| 2804 | }> { |
| 2805 | let remoteStats: { hosts: RemoteHostInfo[]; totalCopied: number } | undefined |
| 2806 | |
| 2807 | // Optionally collect data from remote hosts first (ant-only) |
| 2808 | if (process.env.USER_TYPE === 'ant' && options?.collectRemote) { |
| 2809 | const destDir = join(getClaudeConfigHomeDir(), 'projects') |
| 2810 | const { hosts, totalCopied } = await collectAllRemoteHostData(destDir) |
| 2811 | remoteStats = { hosts, totalCopied } |
| 2812 | } |
| 2813 | |
| 2814 | // Phase 1: Lite scan — filesystem metadata only (no JSONL parsing) |
| 2815 | const allScannedSessions = await scanAllSessions() |
| 2816 | const totalSessionsScanned = allScannedSessions.length |
| 2817 | |
| 2818 | // Phase 2: Load SessionMeta — use cache where available, parse only uncached |
| 2819 | // Read cached metas in parallel batches to avoid blocking the event loop |
| 2820 | const META_BATCH_SIZE = 50 |
| 2821 | const MAX_SESSIONS_TO_LOAD = 200 |
| 2822 | let allMetas: SessionMeta[] = [] |
| 2823 | const uncachedSessions: LiteSessionInfo[] = [] |
| 2824 | |
| 2825 | for (let i = 0; i < allScannedSessions.length; i += META_BATCH_SIZE) { |
| 2826 | const batch = allScannedSessions.slice(i, i + META_BATCH_SIZE) |
| 2827 | const results = await Promise.all( |
| 2828 | batch.map(async sessionInfo => ({ |
| 2829 | sessionInfo, |
| 2830 | cached: await loadCachedSessionMeta(sessionInfo.sessionId), |
| 2831 | })), |
| 2832 | ) |
| 2833 | for (const { sessionInfo, cached } of results) { |
| 2834 | if (cached) { |
| 2835 | allMetas.push(cached) |
| 2836 | } else if (uncachedSessions.length < MAX_SESSIONS_TO_LOAD) { |
| 2837 | uncachedSessions.push(sessionInfo) |
| 2838 | } |
| 2839 | } |
| 2840 | } |
| 2841 | |
| 2842 | // Load full message data only for uncached sessions and compute SessionMeta |
| 2843 | const logsForFacets = new Map<string, LogOption>() |
| 2844 | |
| 2845 | // Filter out /insights meta-sessions (facet extraction API calls get logged as sessions) |
| 2846 | const isMetaSession = (log: LogOption): boolean => { |
| 2847 | for (const msg of log.messages.slice(0, 5)) { |
| 2848 | if (msg.type === 'user' && msg.message) { |
| 2849 | const content = msg.message.content |
| 2850 | if (typeof content === 'string') { |
| 2851 | if ( |
| 2852 | content.includes('RESPOND WITH ONLY A VALID JSON OBJECT') || |
| 2853 | content.includes('record_facets') |
no test coverage detected