mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(analytics): track CLI web daily active users via shared AnalyticsService
- Extract shared AnalyticsService to packages/node-runtime using direct Aptabase HTTP API (removes @aptabase/electron dependency) - Add POST /_web/telemetry/track route for generic event reporting - Desktop: migrate analytics.json from userData to systemDir (~/.chatlab/), trigger daily active from renderer via new IPC so user-selected locale is used - CLI: inject APTABASE_APP_KEY at build time, create AnalyticsService on server start, report app_active with platform: cli-web - Both platforms report user-selected locale via platform service abstraction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
46066d7a5f
commit
c8cccbefd4
19 files changed
+206
-153
No files matched your search
@@ -36,6 +36,7 @@ import {
|
||||
createNodeDataDirSwitch,
|
||||
getDefaultNodeUserDataDir,
|
||||
getPendingNodeDataDirMigration,
|
||||
AnalyticsService,
|
||||
type SemanticIndexRuntime,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { registerSharedRoutes } from '@openchatlab/http-routes'
|
||||
@@ -103,6 +104,10 @@ export function registerWebRoutes(
|
||||
|
||||
const semanticIndexService = options?.semanticIndexService
|
||||
|
||||
const analyticsService = process.env.APTABASE_APP_KEY
|
||||
? new AnalyticsService(resolvedPathProvider.getSystemDir(), process.env.APTABASE_APP_KEY, getVersion())
|
||||
: undefined
|
||||
|
||||
registerSharedRoutes(
|
||||
server,
|
||||
{
|
||||
@@ -112,6 +117,7 @@ export function registerWebRoutes(
|
||||
getVersion,
|
||||
nativeBinding: options?.nativeBinding,
|
||||
semanticIndexService,
|
||||
analyticsService,
|
||||
openDirectory: openDirectoryPath,
|
||||
showInFolder: showPathInFolder,
|
||||
downloadsDir: resolvedPathProvider.getDownloadsDir(),
|
||||
|
||||
@@ -14,6 +14,9 @@ export default defineConfig({
|
||||
clean: true,
|
||||
target: 'node20',
|
||||
platform: 'node',
|
||||
define: {
|
||||
'process.env.APTABASE_APP_KEY': JSON.stringify(process.env.APTABASE_APP_KEY || ''),
|
||||
},
|
||||
noExternal: [/^@openchatlab\//, 'chatlab-mcp', 'stream-json'],
|
||||
external: ['better-sqlite3', '@node-rs/jieba'],
|
||||
banner: {
|
||||
|
||||
+33
-132
@@ -1,158 +1,59 @@
|
||||
/**
|
||||
* 应用分析模块
|
||||
* 使用 Aptabase 进行匿名使用统计
|
||||
*/
|
||||
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { initialize, trackEvent } from '@aptabase/electron/main'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { AnalyticsService } from '@openchatlab/node-runtime'
|
||||
import { getSystemDataDir } from './paths'
|
||||
|
||||
// AppKey
|
||||
const ANALYTICS_APP_KEY = process.env.APTABASE_APP_KEY
|
||||
const APTABASE_APP_KEY = process.env.APTABASE_APP_KEY
|
||||
|
||||
// 分析数据存储路径
|
||||
function getAnalyticsPath(): string {
|
||||
return path.join(app.getPath('userData'), 'analytics.json')
|
||||
}
|
||||
let _service: AnalyticsService | null = null
|
||||
|
||||
// 分析数据结构
|
||||
interface AnalyticsData {
|
||||
lastReportDate: string | null
|
||||
firstReportDate: string | null // 用于判断新老用户
|
||||
enabled: boolean // 是否启用统计
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultAnalyticsData: AnalyticsData = {
|
||||
lastReportDate: null,
|
||||
firstReportDate: null,
|
||||
enabled: true, // 默认启用
|
||||
}
|
||||
|
||||
// 读取分析数据
|
||||
function loadAnalyticsData(): AnalyticsData {
|
||||
function migrateAnalyticsIfNeeded(systemDir: string): void {
|
||||
const newPath = path.join(systemDir, 'analytics.json')
|
||||
if (fs.existsSync(newPath)) return
|
||||
try {
|
||||
const filePath = getAnalyticsPath()
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = fs.readFileSync(filePath, 'utf-8')
|
||||
return { ...defaultAnalyticsData, ...JSON.parse(data) }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Analytics] Failed to read analytics data:', error)
|
||||
}
|
||||
return { ...defaultAnalyticsData }
|
||||
}
|
||||
|
||||
// 保存分析数据
|
||||
function saveAnalyticsData(data: AnalyticsData): void {
|
||||
try {
|
||||
const filePath = getAnalyticsPath()
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
} catch (error) {
|
||||
console.error('[Analytics] Failed to save analytics data:', error)
|
||||
const oldPath = path.join(app.getPath('userData'), 'analytics.json')
|
||||
if (fs.existsSync(oldPath)) fs.copyFileSync(oldPath, newPath)
|
||||
} catch (_e) {
|
||||
// Non-critical migration, ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// 获取今天的日期字符串 (YYYY-MM-DD)
|
||||
function getTodayString(): string {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
function getService(): AnalyticsService | null {
|
||||
if (!APTABASE_APP_KEY) return null
|
||||
if (!_service) {
|
||||
const systemDir = getSystemDataDir()
|
||||
migrateAnalyticsIfNeeded(systemDir)
|
||||
_service = new AnalyticsService(systemDir, APTABASE_APP_KEY, app.getVersion())
|
||||
}
|
||||
return _service
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化分析模块
|
||||
* 必须在 app.whenReady() 之前调用
|
||||
*/
|
||||
export function initAnalytics(): void {
|
||||
if (!ANALYTICS_APP_KEY) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
initialize(ANALYTICS_APP_KEY)
|
||||
console.log('[Analytics] Aptabase initialized')
|
||||
} catch (error) {
|
||||
console.error('[Analytics] Failed to initialize Aptabase:', error)
|
||||
}
|
||||
// Service is initialized lazily; no-op here unless we want eager validation.
|
||||
if (!APTABASE_APP_KEY) return
|
||||
getService()
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Analytics IPC 处理器
|
||||
*/
|
||||
export function registerAnalyticsHandlers(): void {
|
||||
// 获取统计启用状态
|
||||
ipcMain.handle('analytics:getEnabled', () => {
|
||||
return loadAnalyticsData().enabled
|
||||
return getService()?.getEnabled() ?? true
|
||||
})
|
||||
|
||||
// 设置统计启用状态
|
||||
ipcMain.handle('analytics:setEnabled', (_, enabled: boolean) => {
|
||||
const data = loadAnalyticsData()
|
||||
data.enabled = enabled
|
||||
saveAnalyticsData(data)
|
||||
getService()?.setEnabled(enabled)
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('analytics:trackDailyActive', (_, locale: string) => {
|
||||
getService()
|
||||
?.trackDailyActive({ platform: 'desktop', locale })
|
||||
.catch((e) => console.error('[Analytics] Failed to report daily active:', e))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报每日活跃事件
|
||||
*/
|
||||
export function trackDailyActive(): void {
|
||||
if (!ANALYTICS_APP_KEY) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = loadAnalyticsData()
|
||||
|
||||
// 检查是否启用统计
|
||||
if (!data.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const today = getTodayString()
|
||||
const isNew = data.firstReportDate === null
|
||||
|
||||
// 新用户记录首次使用日期
|
||||
if (isNew) {
|
||||
data.firstReportDate = today
|
||||
}
|
||||
|
||||
// 检查今天是否已经上报过
|
||||
if (data.lastReportDate === today) {
|
||||
if (isNew) {
|
||||
saveAnalyticsData(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 上报每日活跃事件
|
||||
trackEvent(isNew ? 'app_active_new' : 'app_active', { locale: app.getLocale() })
|
||||
|
||||
data.lastReportDate = today
|
||||
saveAnalyticsData(data)
|
||||
} catch (error) {
|
||||
console.error('[Analytics] Failed to report daily active:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件上报
|
||||
*/
|
||||
export function trackAppEvent(eventName: string, properties?: Record<string, string | number>): void {
|
||||
if (!ANALYTICS_APP_KEY) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否启用统计
|
||||
if (!loadAnalyticsData().enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
trackEvent(eventName, properties)
|
||||
} catch (error) {
|
||||
console.error(`[Analytics] Failed to report event ${eventName}:`, error)
|
||||
}
|
||||
getService()
|
||||
?.track(eventName, properties)
|
||||
.catch((e) => console.error(`[Analytics] Failed to report event ${eventName}:`, e))
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { checkUpdate } from './update'
|
||||
import mainIpcMain, { cleanup } from './ipcMain'
|
||||
import { startInternalServer, stopInternalServer, registerInternalApiIpc } from './internal-api'
|
||||
import { getPathProvider } from './path-context'
|
||||
import { initAnalytics, trackDailyActive } from './analytics'
|
||||
import { initAnalytics } from './analytics'
|
||||
import { initProxy } from './network/proxy'
|
||||
import {
|
||||
needsLegacyMigration,
|
||||
@@ -306,9 +306,6 @@ class MainProcess {
|
||||
// 设置Windows应用程序用户模型id
|
||||
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
|
||||
|
||||
// 记录日活(用于统计操作系统版本、客户端版本,便于更好的适配客户端)
|
||||
trackDailyActive()
|
||||
|
||||
// 启动 Internal API Server(硬依赖:失败则退出应用)
|
||||
try {
|
||||
await startInternalServer(getPathProvider())
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"build:win": "pnpm run build && electron-builder --win --config electron-builder.yml -p never"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aptabase/electron": "^0.3.1",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@fastify/multipart": "^10.0.0",
|
||||
|
||||
@@ -92,6 +92,9 @@ export const extendedApi = {
|
||||
setAnalyticsEnabled: (enabled: boolean): Promise<{ success: boolean }> => {
|
||||
return ipcRenderer.invoke('analytics:setEnabled', enabled)
|
||||
},
|
||||
trackDailyActive: (locale: string): Promise<void> => {
|
||||
return ipcRenderer.invoke('analytics:trackDailyActive', locale)
|
||||
},
|
||||
/**
|
||||
* 重启应用
|
||||
*/
|
||||
|
||||
Vendored
+1
@@ -103,6 +103,7 @@ interface Api {
|
||||
fetchRemoteConfig: (url: string) => Promise<{ success: boolean; data?: unknown; error?: string }>
|
||||
getAnalyticsEnabled: () => Promise<boolean>
|
||||
setAnalyticsEnabled: (enabled: boolean) => Promise<{ success: boolean }>
|
||||
trackDailyActive: (locale: string) => Promise<void>
|
||||
relaunch: () => Promise<void>
|
||||
getOpenAtLogin: () => Promise<boolean>
|
||||
setOpenAtLogin: (enabled: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import type { ChartAutoMode } from '@openchatlab/shared-types'
|
||||
import type { AuthProfile } from '@openchatlab/config'
|
||||
import type { AnalyticsService } from '@openchatlab/node-runtime'
|
||||
import type {
|
||||
DatabaseManager,
|
||||
DataDirSwitchResult,
|
||||
@@ -66,6 +67,9 @@ export interface HttpRouteContext {
|
||||
resolveApiKey?: (provider: string, authProfile?: string) => string
|
||||
writeAuthProfile?: (name: string, profile: AuthProfile) => void
|
||||
|
||||
/** Analytics tracking service — optional, telemetry routes silently skip when absent */
|
||||
analyticsService?: AnalyticsService
|
||||
|
||||
/** Cache/storage — platform-specific (optional) */
|
||||
openDirectory?: (dirPath: string) => Promise<void>
|
||||
showInFolder?: (filePath: string) => Promise<void>
|
||||
|
||||
@@ -39,3 +39,4 @@ export { registerAiSummaryRoutes } from './routes/web/ai-summaries'
|
||||
export { registerAiLlmStreamRoutes } from './routes/web/ai-llm-stream'
|
||||
export { registerAiAgentStreamRoutes } from './routes/web/ai-agent-stream'
|
||||
export { registerAiToolRoutes } from './routes/web/ai-tools'
|
||||
export { registerTelemetryRoutes } from './routes/web/telemetry'
|
||||
@@ -29,6 +29,7 @@ import { registerAiSummaryRoutes } from './routes/web/ai-summaries'
|
||||
import { registerSemanticIndexRoutes } from './routes/web/ai-semantic-index'
|
||||
import { registerMergeRoutes } from './routes/web/merge'
|
||||
import { registerCacheRoutes } from './routes/web/cache'
|
||||
import { registerTelemetryRoutes } from './routes/web/telemetry'
|
||||
|
||||
export interface SharedRouteOptions {
|
||||
/** When true, AI routes will throw on missing dependencies instead of silently skipping */
|
||||
@@ -90,4 +91,7 @@ export function registerSharedRoutes(
|
||||
|
||||
// Cache/storage routes
|
||||
registerCacheRoutes(server, resolvedCtx)
|
||||
|
||||
// Telemetry routes
|
||||
registerTelemetryRoutes(server, resolvedCtx)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { HttpRouteContext } from '../../context'
|
||||
|
||||
export function registerTelemetryRoutes(server: FastifyInstance, ctx: HttpRouteContext): void {
|
||||
server.post<{ Body: { eventName: string; properties?: Record<string, string | number> } }>(
|
||||
'/_web/telemetry/track',
|
||||
async (req, reply) => {
|
||||
if (!ctx.analyticsService) return { ok: true }
|
||||
const { eventName, properties } = req.body ?? {}
|
||||
if (!eventName) return reply.code(400).send({ error: 'eventName required' })
|
||||
if (eventName === 'app_active') {
|
||||
await ctx.analyticsService.trackDailyActive(properties)
|
||||
} else {
|
||||
await ctx.analyticsService.track(eventName, properties)
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { LLMConfigStore, type ConfigStorage, type AIConfigStore } from '../llm-config-store'
|
||||
import { LLMConfigStore, type ConfigStorage, type AIConfigStore, type AIServiceConfig } from '../llm-config-store'
|
||||
|
||||
function createMemoryStorage(): ConfigStorage & { data: Map<string, unknown> } {
|
||||
const data = new Map<string, unknown>()
|
||||
@@ -204,7 +204,7 @@ describe('LLMConfigStore', () => {
|
||||
const deleted: string[] = []
|
||||
const storeWithHook = new LLMConfigStore(storage, {
|
||||
generateId: () => `id-${++idCounter}`,
|
||||
onApiKeyCreated: (config, apiKey) => {
|
||||
onApiKeyCreated: (config, _apiKey) => {
|
||||
const name = config.name.toLowerCase().replace(/\s+/g, '-')
|
||||
;(config as unknown as Record<string, unknown>).authProfile = name
|
||||
return name
|
||||
@@ -224,7 +224,7 @@ describe('LLMConfigStore', () => {
|
||||
const deleted: string[] = []
|
||||
const storeWithHook = new LLMConfigStore(storage, {
|
||||
generateId: () => `id-${++idCounter}`,
|
||||
onApiKeyCreated: (config, apiKey) => {
|
||||
onApiKeyCreated: (config, _apiKey) => {
|
||||
const name = config.name.toLowerCase().replace(/\s+/g, '-')
|
||||
;(config as unknown as Record<string, unknown>).authProfile = name
|
||||
return name
|
||||
|
||||
@@ -339,7 +339,8 @@ export { Type, completeSimple, streamSimple, runSimpleLlmStream } from './ai'
|
||||
export type { LlmStreamChunk, RunSimpleLlmStreamOptions } from './ai'
|
||||
export type { PiModel, PiApi, PiMessage, PiUsage, PiTextContent, PiAssistantMessage } from './ai'
|
||||
|
||||
// Shared application services (session / member / index / summary / export)
|
||||
// Shared application services (session / member / index / summary / export / analytics)
|
||||
export { AnalyticsService } from './services/analytics'
|
||||
export * as sessionService from './services/session-service'
|
||||
export * as memberService from './services/member-service'
|
||||
export * as sessionIndexService from './services/session-index-service'
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const REGIONS: Record<string, string> = {
|
||||
US: 'https://us.aptabase.com',
|
||||
EU: 'https://eu.aptabase.com',
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
lastReportDate: string | null
|
||||
firstReportDate: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_DATA: AnalyticsData = { lastReportDate: null, firstReportDate: null, enabled: true }
|
||||
|
||||
export class AnalyticsService {
|
||||
private readonly dataPath: string
|
||||
private readonly endpoint: string | null
|
||||
private readonly appKey: string
|
||||
private readonly appVersion: string
|
||||
private readonly sessionId: string
|
||||
|
||||
constructor(systemDir: string, appKey: string, appVersion: string) {
|
||||
this.dataPath = path.join(systemDir, 'analytics.json')
|
||||
this.appKey = appKey
|
||||
this.appVersion = appVersion
|
||||
this.sessionId = `${Math.floor(Date.now() / 1000)}${Math.floor(Math.random() * 1e8)
|
||||
.toString()
|
||||
.padStart(8, '0')}`
|
||||
const parts = appKey.split('-')
|
||||
const base = parts.length === 3 ? REGIONS[parts[1]] : undefined
|
||||
this.endpoint = base ? `${base}/api/v0/event` : null
|
||||
}
|
||||
|
||||
private load(): AnalyticsData {
|
||||
try {
|
||||
if (fs.existsSync(this.dataPath)) {
|
||||
return { ...DEFAULT_DATA, ...JSON.parse(fs.readFileSync(this.dataPath, 'utf-8')) }
|
||||
}
|
||||
} catch (_e) {
|
||||
// Corrupted or unreadable file — fall back to defaults
|
||||
}
|
||||
return { ...DEFAULT_DATA }
|
||||
}
|
||||
|
||||
private save(data: AnalyticsData): void {
|
||||
try {
|
||||
fs.writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8')
|
||||
} catch (e) {
|
||||
console.error('[Analytics] Failed to save:', e)
|
||||
}
|
||||
}
|
||||
|
||||
getEnabled(): boolean {
|
||||
return this.load().enabled
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
const data = this.load()
|
||||
data.enabled = enabled
|
||||
this.save(data)
|
||||
}
|
||||
|
||||
async track(eventName: string, props?: Record<string, string | number>): Promise<void> {
|
||||
if (!this.endpoint) return
|
||||
if (!this.getEnabled()) return
|
||||
try {
|
||||
await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'App-Key': this.appKey },
|
||||
body: JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
sessionId: this.sessionId,
|
||||
eventName,
|
||||
systemProps: {
|
||||
isDebug: false,
|
||||
locale: '',
|
||||
osName: process.platform,
|
||||
osVersion: '',
|
||||
engineName: 'Node.js',
|
||||
engineVersion: process.versions.node,
|
||||
appVersion: this.appVersion,
|
||||
sdkVersion: 'chatlab@1',
|
||||
},
|
||||
props,
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(`[Analytics] Failed to track ${eventName}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
async trackDailyActive(props?: Record<string, string | number>): Promise<void> {
|
||||
if (!this.endpoint) return
|
||||
const data = this.load()
|
||||
if (!data.enabled) return
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const isNew = data.firstReportDate === null
|
||||
if (isNew) data.firstReportDate = today
|
||||
if (data.lastReportDate === today) {
|
||||
if (isNew) this.save(data)
|
||||
return
|
||||
}
|
||||
await this.track(isNew ? 'app_active_new' : 'app_active', props)
|
||||
data.lastReportDate = today
|
||||
this.save(data)
|
||||
}
|
||||
}
|
||||
Generated
-12
@@ -173,9 +173,6 @@ importers:
|
||||
|
||||
apps/desktop:
|
||||
dependencies:
|
||||
'@aptabase/electron':
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(electron@35.7.5)
|
||||
'@electron-toolkit/preload':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.2(electron@35.7.5)
|
||||
@@ -457,11 +454,6 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@aptabase/electron@0.3.1':
|
||||
resolution: {integrity: sha512-FECaGsjuoQu70F+M6V1evdgLP7yaq/sne9fC60AZZ6B9RsCDlxFBnJzdq3+xevHlUx6AB5o9ygUhQ+ONT9EqiA==}
|
||||
peerDependencies:
|
||||
electron: '>= 3.x'
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -6165,10 +6157,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
zod: 3.25.76
|
||||
|
||||
'@aptabase/electron@0.3.1(electron@35.7.5)':
|
||||
dependencies:
|
||||
electron: 35.7.5
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
|
||||
@@ -20,6 +20,7 @@ import { initPreferencesSync } from '@/composables/usePreferencesSync'
|
||||
import { useWindowsTitleBarOverlay } from '@/composables/useWindowsTitleBarOverlay'
|
||||
import { configureHttpClient } from '@/services/utils/http'
|
||||
import { IS_ELECTRON } from '@/utils/platform'
|
||||
import { usePlatformService } from '@/services'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -61,6 +62,9 @@ async function initializeApp() {
|
||||
await settingsStore.initLocale()
|
||||
llmStore.init()
|
||||
await sessionStore.loadSessions()
|
||||
usePlatformService()
|
||||
.trackDailyActive(settingsStore.locale)
|
||||
.catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('App initialization failed:', err)
|
||||
initError.value = err instanceof Error ? err.message : String(err)
|
||||
|
||||
@@ -33,6 +33,10 @@ export class ElectronPlatformAdapter implements PlatformAdapter {
|
||||
return window.api.app.setAnalyticsEnabled(enabled)
|
||||
}
|
||||
|
||||
trackDailyActive(locale: string): Promise<void> {
|
||||
return window.api.app.trackDailyActive(locale)
|
||||
}
|
||||
|
||||
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogResult> {
|
||||
return window.api.dialog.showOpenDialog(options as Electron.OpenDialogOptions)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface PlatformAdapter {
|
||||
|
||||
getAnalyticsEnabled(): Promise<boolean>
|
||||
setAnalyticsEnabled(enabled: boolean): Promise<{ success: boolean }>
|
||||
trackDailyActive(locale: string): Promise<void>
|
||||
|
||||
showOpenDialog(options: OpenDialogOptions): Promise<OpenDialogResult>
|
||||
copyImageToClipboard(dataUrl: string): Promise<{ success: boolean; error?: string }>
|
||||
|
||||
@@ -52,6 +52,14 @@ export class WebPlatformAdapter implements PlatformAdapter {
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
async trackDailyActive(locale: string): Promise<void> {
|
||||
await fetchWithAuth('/_web/telemetry/track', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ eventName: 'app_active', properties: { platform: 'cli-web', locale } }),
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async showOpenDialog(_options: OpenDialogOptions): Promise<OpenDialogResult> {
|
||||
return { canceled: true, filePaths: [] }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user