mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-09 00:20:06 +00:00
fix(analytics): avoid bridge double counting in native collectors
This commit is contained in:
@@ -16,6 +16,10 @@ interface CodexTokenSnapshot {
|
|||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCliProxyBackedProvider(value: string | undefined): boolean {
|
||||||
|
return value === 'cliproxy' || value === 'ccs_runtime';
|
||||||
|
}
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
}
|
}
|
||||||
@@ -123,7 +127,7 @@ async function parseRolloutFile(
|
|||||||
!isObject(parsed.payload) ||
|
!isObject(parsed.payload) ||
|
||||||
parsed.payload.type !== 'token_count' ||
|
parsed.payload.type !== 'token_count' ||
|
||||||
!sessionId ||
|
!sessionId ||
|
||||||
(modelProvider === 'cliproxy' && !includeCliproxySessions)
|
(isCliProxyBackedProvider(modelProvider) && !includeCliproxySessions)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ interface DroidSessionMetadata {
|
|||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DROID_COST_BATCH_SIZE = 1000;
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
}
|
}
|
||||||
@@ -149,11 +151,19 @@ export async function scanDroidNativeUsageEntries(
|
|||||||
const costsDbPath = path.join(factoryDir, 'costs.db');
|
const costsDbPath = path.join(factoryDir, 'costs.db');
|
||||||
if (!fs.existsSync(costsDbPath)) return [];
|
if (!fs.existsSync(costsDbPath)) return [];
|
||||||
|
|
||||||
const rows =
|
const rows: Array<Record<string, unknown>> = [];
|
||||||
(await query(
|
let offset = 0;
|
||||||
|
while (true) {
|
||||||
|
const batch = await query(
|
||||||
costsDbPath,
|
costsDbPath,
|
||||||
'SELECT session_id, timestamp, input_tokens, output_tokens FROM costs ORDER BY timestamp ASC;'
|
`SELECT session_id, timestamp, input_tokens, output_tokens FROM costs ` +
|
||||||
)) ?? [];
|
`ORDER BY timestamp ASC LIMIT ${DROID_COST_BATCH_SIZE} OFFSET ${offset};`
|
||||||
|
);
|
||||||
|
if (!batch.length) break;
|
||||||
|
rows.push(...batch);
|
||||||
|
if (batch.length < DROID_COST_BATCH_SIZE) break;
|
||||||
|
offset += batch.length;
|
||||||
|
}
|
||||||
if (!rows.length) return [];
|
if (!rows.length) return [];
|
||||||
|
|
||||||
const metadata = loadSessionMetadata(factoryDir);
|
const metadata = loadSessionMetadata(factoryDir);
|
||||||
@@ -168,11 +178,11 @@ export async function scanDroidNativeUsageEntries(
|
|||||||
if (!sessionId || !timestamp || inputTokens === null || outputTokens === null) continue;
|
if (!sessionId || !timestamp || inputTokens === null || outputTokens === null) continue;
|
||||||
|
|
||||||
const session = metadata.get(sessionId);
|
const session = metadata.get(sessionId);
|
||||||
if (
|
if (!session) {
|
||||||
session &&
|
continue;
|
||||||
!options.includeCcsManagedSessions &&
|
}
|
||||||
isCcsManagedSelector(session.rawSelector)
|
|
||||||
) {
|
if (!options.includeCcsManagedSessions && isCcsManagedSelector(session.rawSelector)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,11 +191,11 @@ export async function scanDroidNativeUsageEntries(
|
|||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreationTokens: 0,
|
cacheCreationTokens: 0,
|
||||||
cacheReadTokens: 0,
|
cacheReadTokens: 0,
|
||||||
model: session?.model ?? 'unknown-droid-model',
|
model: session.model,
|
||||||
sessionId,
|
sessionId,
|
||||||
timestamp,
|
timestamp,
|
||||||
projectPath: session?.projectPath ?? '/',
|
projectPath: session.projectPath,
|
||||||
version: session?.version,
|
version: session.version,
|
||||||
target: 'droid',
|
target: 'droid',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ function isCommandMissing(error: unknown): boolean {
|
|||||||
return nodeError.code === 'ENOENT' || /not found/i.test(nodeError.message);
|
return nodeError.code === 'ENOENT' || /not found/i.test(nodeError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function querySqliteJson(
|
export async function querySqliteJson(dbPath: string, sql: string): Promise<SqliteJsonRow[]> {
|
||||||
dbPath: string,
|
|
||||||
sql: string
|
|
||||||
): Promise<SqliteJsonRow[] | null> {
|
|
||||||
if (!fs.existsSync(dbPath)) {
|
if (!fs.existsSync(dbPath)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -34,8 +31,10 @@ export async function querySqliteJson(
|
|||||||
return Array.isArray(parsed) ? (parsed as SqliteJsonRow[]) : [];
|
return Array.isArray(parsed) ? (parsed as SqliteJsonRow[]) : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isCommandMissing(error)) {
|
if (isCommandMissing(error)) {
|
||||||
return null;
|
throw new Error('sqlite3 command not available');
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(`sqlite3 query failed for ${dbPath}: ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,4 +173,15 @@ describe('codex native usage collector', () => {
|
|||||||
|
|
||||||
expect(entries).toHaveLength(0);
|
expect(entries).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('also skips ccs_runtime-backed codex bridge sessions by default', async () => {
|
||||||
|
writeCodexRollout(tempRoot, { modelProvider: 'ccs_runtime' });
|
||||||
|
|
||||||
|
const entries = await scanCodexNativeUsageEntries({
|
||||||
|
env: { CODEX_HOME: tempRoot },
|
||||||
|
homeDir: tempRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(entries).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,4 +131,21 @@ describe('droid native usage collector', () => {
|
|||||||
|
|
||||||
expect(entries).toHaveLength(0);
|
expect(entries).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces sqlite failures instead of flattening them into an empty result', async () => {
|
||||||
|
writeDroidGlobalSettings(tempRoot);
|
||||||
|
writeDroidSessionFixture(tempRoot);
|
||||||
|
|
||||||
|
const querySqliteJson: DroidSqliteQuery = async () => {
|
||||||
|
throw new Error('sqlite blew up');
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
scanDroidNativeUsageEntries({
|
||||||
|
env: { CCS_HOME: tempRoot },
|
||||||
|
homeDir: tempRoot,
|
||||||
|
querySqliteJson,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('sqlite blew up');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -280,4 +280,19 @@ describe('usage aggregator native runtime integration', () => {
|
|||||||
});
|
});
|
||||||
expect(daily[0].modelsUsed).not.toContain('gpt-5');
|
expect(daily[0].modelsUsed).not.toContain('gpt-5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores ccs_runtime-backed codex bridge rollouts to avoid double counting', async () => {
|
||||||
|
writeCodexFixture('ccs_runtime');
|
||||||
|
aggregator.clearUsageCache();
|
||||||
|
|
||||||
|
const daily = await aggregator.getCachedDailyData();
|
||||||
|
|
||||||
|
expect(daily).toHaveLength(1);
|
||||||
|
expect(daily[0]).toMatchObject({
|
||||||
|
inputTokens: 150,
|
||||||
|
outputTokens: 50,
|
||||||
|
cacheReadTokens: 5,
|
||||||
|
});
|
||||||
|
expect(daily[0].modelsUsed).not.toContain('gpt-5');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user