mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-04 13:11:56 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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
16
pnpm-lock.yaml
generated
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
47
src/main/services/AnalyticsService.ts
Normal file
47
src/main/services/AnalyticsService.ts
Normal 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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user