feat: integrate analytics-client for token usage tracking (#12809)

Add @cherrystudio/analytics-client integration to track AI token usage:
- Create AnalyticsService in main process for managing AnalyticsClient
- Add IPC channel for renderer to report token usage
- Track token usage on message completion with provider, model, and token counts
- Respect enableDataCollection setting for user privacy
This commit is contained in:
亢奋猫
2026-02-10 11:19:03 +08:00
committed by GitHub
parent 60e4e7722f
commit 1734467fe0
8 changed files with 95 additions and 1 deletions

View File

@@ -118,6 +118,7 @@
"@aws-sdk/client-s3": "^3.910.0",
"@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.9",
"@cherrystudio/analytics-client": "^1.1.0",
"@cherrystudio/embedjs": "0.1.31",
"@cherrystudio/embedjs-interfaces": "0.1.31",
"@cherrystudio/embedjs-libsql": "0.1.31",

View File

@@ -422,5 +422,8 @@ export enum IpcChannel {
OpenClaw_CheckHealth = 'openclaw:check-health',
OpenClaw_GetDashboardUrl = 'openclaw:get-dashboard-url',
OpenClaw_SyncConfig = 'openclaw:sync-config',
OpenClaw_GetChannels = 'openclaw:get-channels'
OpenClaw_GetChannels = 'openclaw:get-channels',
// Analytics
Analytics_TrackTokenUsage = 'analytics:track-token-usage'
}

16
pnpm-lock.yaml generated
View File

@@ -233,6 +233,9 @@ importers:
'@cherrystudio/ai-core':
specifier: workspace:^1.0.9
version: link:packages/aiCore
'@cherrystudio/analytics-client':
specifier: ^1.1.0
version: 1.1.0(typescript@5.8.3)
'@cherrystudio/embedjs':
specifier: 0.1.31
version: 0.1.31(@langchain/core@1.0.2(patch_hash=8dc787a82cebafe8b23c8826f25f29aca64fc8b43a0a1878e0010782e4da96ed)(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.15.0(ws@8.19.0)(zod@4.3.4)))(@langchain/ollama@0.1.6(@langchain/core@1.0.2(patch_hash=8dc787a82cebafe8b23c8826f25f29aca64fc8b43a0a1878e0010782e4da96ed)(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.15.0(ws@8.19.0)(zod@4.3.4))))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(axios@1.13.2)(cheerio@1.1.2)(handlebars@4.7.8)(openai@6.15.0(ws@8.19.0)(zod@4.3.4))(ws@8.19.0)
@@ -1960,6 +1963,15 @@ packages:
'@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@cherrystudio/analytics-client@1.1.0':
resolution: {integrity: sha512-P+X+aCkP+4YLgBrO9SiicQyFh23TeIm85flsmVrJWgPjRzbzNMqGu9VDE8ndZlZaRohJ9UiN94MVIBRJiU9+pw==}
engines: {node: '>=16.0.0'}
peerDependencies:
typescript: '>=4.7.0'
peerDependenciesMeta:
typescript:
optional: true
'@cherrystudio/embedjs-interfaces@0.1.30':
resolution: {integrity: sha512-ShUzWAUQVmX4LsrAZBO3pqusJV8vXVFuxmdmMjeVXWlynORnmRx2CQ1s6xghyxarBUzFCUsQOrOmXO+9ZQoS/Q==}
@@ -13388,6 +13400,10 @@ snapshots:
'@cfworker/json-schema@4.1.1': {}
'@cherrystudio/analytics-client@1.1.0(typescript@5.8.3)':
optionalDependencies:
typescript: 5.8.3
'@cherrystudio/embedjs-interfaces@0.1.30(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.15.0(ws@8.19.0)(zod@4.3.4))':
dependencies:
'@langchain/core': 1.0.2(patch_hash=8dc787a82cebafe8b23c8826f25f29aca64fc8b43a0a1878e0010782e4da96ed)(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.15.0(ws@8.19.0)(zod@4.3.4))

View File

@@ -16,6 +16,7 @@ import process from 'node:process'
import { registerIpc } from './ipc'
import { agentService } from './services/agents'
import { analyticsService } from './services/AnalyticsService'
import { apiServerService } from './services/ApiServerService'
import { appMenuService } from './services/AppMenuService'
import { configManager } from './services/ConfigManager'
@@ -156,6 +157,7 @@ if (!app.requestSingleInstanceLock()) {
nodeTraceService.init()
powerMonitorService.init()
analyticsService.init()
app.on('activate', function () {
const mainWindow = windowService.getMainWindow()
@@ -268,6 +270,7 @@ if (!app.requestSingleInstanceLock()) {
}
try {
await analyticsService.destroy()
await openClawService.stopGateway()
await mcpService.cleanup()
await apiServerService.stop()

View File

@@ -39,6 +39,7 @@ import fontList from 'font-list'
import { agentMessageRepository } from './services/agents/database'
import { PluginService } from './services/agents/plugins/PluginService'
import { analyticsService } from './services/AnalyticsService'
import { apiServerService } from './services/ApiServerService'
import appService from './services/AppService'
import AppUpdater from './services/AppUpdater'
@@ -1149,4 +1150,7 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App)
ipcMain.handle(IpcChannel.OpenClaw_GetDashboardUrl, openClawService.getDashboardUrl)
ipcMain.handle(IpcChannel.OpenClaw_SyncConfig, openClawService.syncProviderConfig)
ipcMain.handle(IpcChannel.OpenClaw_GetChannels, openClawService.getChannelStatus)
// Analytics
ipcMain.handle(IpcChannel.Analytics_TrackTokenUsage, analyticsService.trackTokenUsage)
}

View File

@@ -0,0 +1,47 @@
import type { TokenUsageData } from '@cherrystudio/analytics-client'
import { AnalyticsClient } from '@cherrystudio/analytics-client'
import { loggerService } from '@logger'
import { configManager } from './ConfigManager'
const logger = loggerService.withContext('AnalyticsService')
class AnalyticsService {
private client: AnalyticsClient | null = null
private static instance: AnalyticsService
public static getInstance(): AnalyticsService {
if (!AnalyticsService.instance) {
AnalyticsService.instance = new AnalyticsService()
}
return AnalyticsService.instance
}
public init(): void {
if (!configManager.getEnableDataCollection()) {
logger.info('Data collection is disabled, skipping analytics initialization')
return
}
this.client = new AnalyticsClient({
clientId: configManager.getClientId(),
channel: 'cherry-studio',
onError: (error) => logger.error('Analytics error:', error)
})
logger.info('Analytics service initialized')
}
public trackTokenUsage(_: Electron.IpcMainInvokeEvent, data: TokenUsageData): void {
if (!this.client) return
this.client.trackTokenUsage(data)
}
public async destroy(): Promise<void> {
if (!this.client) return
await this.client.destroy()
this.client = null
logger.info('Analytics service destroyed')
}
}
export const analyticsService = AnalyticsService.getInstance()

View File

@@ -1,4 +1,5 @@
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
import type { TokenUsageData } from '@cherrystudio/analytics-client'
import { electronAPI } from '@electron-toolkit/preload'
import type { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import type { SpanContext } from '@opentelemetry/api'
@@ -690,6 +691,9 @@ const api = {
syncConfig: (provider: Provider, primaryModel: Model): Promise<{ success: boolean; message: string }> =>
ipcRenderer.invoke(IpcChannel.OpenClaw_SyncConfig, provider, primaryModel),
getChannels: (): Promise<OpenClawChannelInfo[]> => ipcRenderer.invoke(IpcChannel.OpenClaw_GetChannels)
},
analytics: {
trackTokenUsage: (data: TokenUsageData) => ipcRenderer.invoke(IpcChannel.Analytics_TrackTokenUsage, data)
}
}

View File

@@ -255,6 +255,22 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => {
})
)
await saveUpdatesToDB(assistantMsgId, topicId, messageUpdates, [])
// Track token usage analytics
const provider = assistant?.model?.provider
const model = assistant?.model?.id
if (status === 'success' && response?.usage && provider && model) {
const { prompt_tokens, completion_tokens } = response.usage
if (prompt_tokens > 0 || completion_tokens > 0) {
window.api.analytics.trackTokenUsage({
provider,
model,
input_tokens: prompt_tokens || 0,
output_tokens: completion_tokens || 0
})
}
}
EventEmitter.emit(EVENT_NAMES.MESSAGE_COMPLETE, { id: assistantMsgId, topicId, status })
logger.debug('onComplete finished')
}