feat(stage-*): track provider card clicks and time-to-first-message (#832)

This commit is contained in:
Rin
2025-12-27 16:46:58 +08:00
committed by GitHub
parent b8810943f8
commit ca6a0b24fa
16 changed files with 174 additions and 93 deletions
@@ -2,9 +2,13 @@ import type { Plugin } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import Tres from '@tresjs/core'
import buildTime from '~build/time'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
import { MotionPlugin } from '@vueuse/motion'
import { abbreviatedSha, branch } from '~build/git'
import { version } from '~build/package'
import { createPinia } from 'pinia'
import { setupLayouts } from 'virtual:generated-layouts'
import { createApp } from 'vue'
@@ -38,6 +42,15 @@ import '@fontsource/sniglet'
const pinia = createPinia()
// Initialize analytics
const analyticsStore = useSharedAnalyticsStore(pinia)
analyticsStore.initialize({
version: version ?? 'dev',
commit: abbreviatedSha,
branch,
builtOn: buildTime.toISOString(),
})
const router = createRouter({
history: createWebHashHistory(),
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
@@ -1,16 +1,6 @@
import buildTime from '~build/time'
import posthog from 'posthog-js'
import { abbreviatedSha } from '~build/git'
import { version } from '~build/package'
posthog.init('phc_rljw376z5gt6vXJlc3sTr7hFbXodciY9THEQXIRnW53', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
})
posthog.register({
app_version: version ?? 'dev',
app_commit: abbreviatedSha,
app_build_time: buildTime,
})
@@ -1,24 +1,17 @@
<script setup lang="ts">
import { UTCDate } from '@date-fns/utc'
import { MarkdownRenderer, Progress } from '@proj-airi/stage-ui/components'
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics/index'
import { Button, DoubleCheckButton } from '@proj-airi/ui'
import { useMediaQuery } from '@vueuse/core'
import { abbreviatedSha, branch, committerDate } from '~build/git'
import { formatISO9075 } from 'date-fns'
import { storeToRefs } from 'pinia'
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
import { DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle } from 'vaul-vue'
import { computed, ref } from 'vue'
import packageJson from '../../../package.json'
import { useElectronAutoUpdater } from '../composables/electron-vueuse'
const buildInfo = computed(() => ({
version: packageJson.version,
branch,
commit: abbreviatedSha.substring(0, 7),
builtOn: formatISO9075(new UTCDate(committerDate)),
}))
const analyticsStore = useSharedAnalyticsStore()
const { buildInfo } = storeToRefs(analyticsStore)
const {
state: updateState,
@@ -1,17 +1,13 @@
<script setup lang="ts">
import { UTCDate } from '@date-fns/utc'
import { AboutContent, AboutDialog } from '@proj-airi/stage-ui/components'
import { abbreviatedSha, branch, committerDate } from '~build/git'
import { formatISO9075 } from 'date-fns'
import { computed, ref } from 'vue'
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics/index'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
const show = ref(false)
const localDate = formatISO9075(new UTCDate(committerDate))
const buildInfo = computed(() => ({
branch,
commit: abbreviatedSha.substring(0, 7),
builtOn: localDate,
}))
const analyticsStore = useSharedAnalyticsStore()
const { buildInfo } = storeToRefs(analyticsStore)
const aboutLinks = [
{ label: 'Home', href: 'https://airi.moeru.ai/docs/', icon: 'i-solar:home-smile-outline' },
{ label: 'Documentations', href: 'https://airi.moeru.ai/docs/en/docs/overview/', icon: 'i-solar:document-add-outline' },
+14
View File
@@ -2,10 +2,14 @@ import type { Plugin } from 'vue'
import type { Router, RouteRecordRaw } from 'vue-router'
import Tres from '@tresjs/core'
import buildTime from '~build/time'
import NProgress from 'nprogress'
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
import { useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
import { MotionPlugin } from '@vueuse/motion'
import { abbreviatedSha, branch } from '~build/git'
import { version } from '~build/package'
import { createPinia } from 'pinia'
import { setupLayouts } from 'virtual:generated-layouts'
import { createApp } from 'vue'
@@ -26,6 +30,16 @@ import './styles/main.css'
import 'uno.css'
const pinia = createPinia()
// Initialize analytics
const analyticsStore = useSharedAnalyticsStore(pinia)
analyticsStore.initialize({
version: version ?? 'dev',
commit: abbreviatedSha,
branch,
builtOn: buildTime.toISOString(),
})
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
const routeRecords = setupLayouts(routes as RouteRecordRaw[])
-10
View File
@@ -1,16 +1,6 @@
import buildTime from '~build/time'
import posthog from 'posthog-js'
import { abbreviatedSha } from '~build/git'
import { version } from '~build/package'
posthog.init('phc_pzjziJjrVZpa9SqnQqq0QEKvkmuCPH7GDTA6TbRTEf9', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
})
posthog.register({
app_version: version ?? 'dev',
app_commit: abbreviatedSha,
app_build_time: buildTime,
})
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Alert, ErrorContainer, RadioCardManySelect, RadioCardSimple } from '@proj-airi/stage-ui/components'
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { storeToRefs } from 'pinia'
@@ -22,6 +23,7 @@ const {
} = storeToRefs(consciousnessStore)
const { t } = useI18n()
const { trackProviderClick } = useAnalytics()
watch(activeProvider, async (provider) => {
if (!provider)
@@ -76,6 +78,7 @@ function handleDeleteProvider(providerId: string) {
:value="metadata.id"
:title="metadata.localizedName || 'Unknown'"
:description="metadata.localizedDescription"
@click="trackProviderClick(metadata.id, 'consciousness')"
>
<template #topRight>
<button
@@ -2,7 +2,7 @@
import workletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url'
import { Alert, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components'
import { useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
import { useAnalytics, useAudioAnalyzer, useAudioRecorder } from '@proj-airi/stage-ui/composables'
import { useVAD } from '@proj-airi/stage-ui/stores/ai/models/vad'
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
import { useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing'
@@ -29,6 +29,7 @@ const {
const providersStore = useProvidersStore()
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
const { trackProviderClick } = useAnalytics()
const { stopStream, startStream } = useSettingsAudioDevice()
const { audioInputs, selectedAudioInput, stream } = storeToRefs(useSettingsAudioDevice())
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
@@ -290,6 +291,7 @@ onUnmounted(() => {
:value="metadata.id"
:title="metadata.localizedName || 'Unknown'"
:description="metadata.localizedDescription"
@click="trackProviderClick(metadata.id, 'hearing')"
/>
<RouterLink
to="/settings/providers#transcription"
@@ -10,6 +10,7 @@ import {
TestDummyMarker,
VoiceCardManySelect,
} from '@proj-airi/stage-ui/components'
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import {
@@ -45,6 +46,8 @@ const {
availableVoices,
} = storeToRefs(speechStore)
const { trackProviderClick } = useAnalytics()
const voiceSearchQuery = ref('')
const useSSML = ref(false)
const testText = ref('Hello, my name is AI Assistant')
@@ -198,6 +201,7 @@ function updateCustomModelName(value: string) {
:value="metadata.id"
:title="metadata.localizedName || 'Unknown'"
:description="metadata.localizedDescription"
@click="trackProviderClick(metadata.id, 'speech')"
/>
<RouterLink
to="/settings/providers#speech"
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { IconStatusItem, RippleGrid } from '@proj-airi/stage-ui/components'
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { useRippleGridState } from '@proj-airi/stage-ui/composables/use-ripple-grid-state'
import { useScrollToHash } from '@proj-airi/stage-ui/composables/useScrollToHash'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
@@ -10,6 +11,7 @@ import { useRoute } from 'vue-router'
const route = useRoute()
const providersStore = useProvidersStore()
const { lastClickedIndex, setLastClickedIndex } = useRippleGridState()
const { trackProviderClick } = useAnalytics()
const {
allChatProvidersMetadata,
@@ -114,6 +116,7 @@ useScrollToHash(() => route.hash, {
:icon-image="provider.iconImage"
:to="`/settings/providers/${provider.category}/${provider.id}`"
:configured="provider.configured"
@click="trackProviderClick(provider.id, provider.category)"
/>
</template>
</RippleGrid>
@@ -4,5 +4,6 @@ export * from './llmmarkerParser'
export * from './markdown'
export * from './micvad'
export * from './queues'
export * from './use-analytics'
export * from './use-context-bridge'
export * from './whisper'
@@ -0,0 +1,36 @@
import posthog from 'posthog-js'
import { useSharedAnalyticsStore } from '../stores/analytics'
export function useAnalytics() {
const analyticsStore = useSharedAnalyticsStore()
function trackProviderClick(providerId: string, module: string) {
posthog.capture('provider_card_clicked', {
provider_id: providerId,
module,
})
}
function trackFirstMessage() {
// Only track the first message once
if (analyticsStore.firstMessageTracked)
return
analyticsStore.markFirstMessageTracked()
// Calculate time from app start to message sent
const timeToFirstMessageMs = analyticsStore.appStartTime
? Date.now() - analyticsStore.appStartTime
: null
posthog.capture('first_message_sent', {
time_to_first_message_ms: timeToFirstMessageMs,
})
}
return {
trackProviderClick,
trackFirstMessage,
}
}
@@ -36,48 +36,39 @@ export function useContextBridge() {
broadcastContext(event.data as ContextMessage)
}))
disposeHookFns.push(...[
chatStore.onBeforeMessageComposed(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'before-compose', message, sessionId: chatStore.activeSessionId })
}),
chatStore.onAfterMessageComposed(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'after-compose', message, sessionId: chatStore.activeSessionId })
}),
chatStore.onBeforeSend(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'before-send', message, sessionId: chatStore.activeSessionId })
}),
chatStore.onAfterSend(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'after-send', message, sessionId: chatStore.activeSessionId })
}),
chatStore.onTokenLiteral(async (literal) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'token-literal', literal, sessionId: chatStore.activeSessionId })
}),
chatStore.onTokenSpecial(async (special) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'token-special', special, sessionId: chatStore.activeSessionId })
}),
chatStore.onStreamEnd(async () => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'stream-end', sessionId: chatStore.activeSessionId })
}),
chatStore.onAssistantResponseEnd(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'assistant-end', message, sessionId: chatStore.activeSessionId })
}),
])
disposeHookFns.push(chatStore.onBeforeMessageComposed(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'before-compose', message, sessionId: chatStore.activeSessionId })
}), chatStore.onAfterMessageComposed(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'after-compose', message, sessionId: chatStore.activeSessionId })
}), chatStore.onBeforeSend(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'before-send', message, sessionId: chatStore.activeSessionId })
}), chatStore.onAfterSend(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'after-send', message, sessionId: chatStore.activeSessionId })
}), chatStore.onTokenLiteral(async (literal) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'token-literal', literal, sessionId: chatStore.activeSessionId })
}), chatStore.onTokenSpecial(async (special) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'token-special', special, sessionId: chatStore.activeSessionId })
}), chatStore.onStreamEnd(async () => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'stream-end', sessionId: chatStore.activeSessionId })
}), chatStore.onAssistantResponseEnd(async (message) => {
if (isProcessingRemoteStream)
return
broadcastStreamEvent({ type: 'assistant-end', message, sessionId: chatStore.activeSessionId })
}))
const { stop: stopIncomingStreamWatch } = watch(incomingStreamEvent, async (event) => {
if (!event)
@@ -0,0 +1,48 @@
import type { AboutBuildInfo } from '../../components/scenarios/about/types'
import posthog from 'posthog-js'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSharedAnalyticsStore = defineStore('analytics-shared', () => {
const isInitialized = ref(false)
const buildInfo = ref<AboutBuildInfo>({
version: '',
commit: '',
branch: '',
builtOn: '',
})
const appStartTime = ref<number | null>(null)
const firstMessageTracked = ref(false)
function initialize(info: AboutBuildInfo) {
if (isInitialized.value)
return
buildInfo.value = info
appStartTime.value = Date.now()
// Register metadata with PostHog after buildInfo is set
posthog.register({
app_version: (buildInfo.value.version && buildInfo.value.version !== '0.0.0') ? buildInfo.value.version : 'dev',
app_commit: buildInfo.value.commit,
app_branch: buildInfo.value.branch,
app_build_time: buildInfo.value.builtOn,
})
isInitialized.value = true
}
function markFirstMessageTracked() {
firstMessageTracked.value = true
}
return {
buildInfo,
appStartTime,
firstMessageTracked,
initialize,
markFirstMessageTracked,
}
})
+3
View File
@@ -9,6 +9,7 @@ import { useLocalStorage } from '@vueuse/core'
import { defineStore, storeToRefs } from 'pinia'
import { computed, ref, toRaw, watch } from 'vue'
import { useAnalytics } from '../composables'
import { useLlmmarkerParser } from '../composables/llmmarkerParser'
import { useLLM } from '../stores/llm'
import { createQueue } from '../utils/queue'
@@ -23,6 +24,7 @@ export const CHAT_STREAM_CHANNEL_NAME = 'airi-chat-stream'
export const useChatStore = defineStore('chat', () => {
const { stream, discoverToolsCompatibility } = useLLM()
const { systemPrompt } = storeToRefs(useAiriCardStore())
const { trackFirstMessage } = useAnalytics()
const activeSessionId = useLocalStorage<string>(ACTIVE_SESSION_STORAGE_KEY, 'default')
const sessionMessages = useLocalStorage<Record<string, ChatHistoryItem[]>>(CHAT_STORAGE_KEY, {})
@@ -332,6 +334,7 @@ export const useChatStore = defineStore('chat', () => {
streamingMessage.value = { role: 'assistant', content: '', slices: [], tool_results: [], createdAt: Date.now() }
trackFirstMessage()
try {
await emitBeforeMessageComposedHooks(sendingMessage)
+3 -9
View File
@@ -2114,7 +2114,7 @@ importers:
version: 66.5.11
'@wxt-dev/module-vue':
specifier: ^1.0.3
version: 1.0.3(vite@8.0.0-beta.1(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
vue-tsc:
specifier: ^3.1.8
version: 3.1.8(typescript@5.9.3)
@@ -22852,12 +22852,6 @@ snapshots:
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.1(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53
vite: 8.0.0-beta.1(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
'@vitest/browser-playwright@4.0.16(bufferutil@4.0.9)(msw@2.7.3(@types/node@24.10.4)(typescript@5.9.3))(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
dependencies:
'@vitest/browser': 4.0.16(bufferutil@4.0.9)(msw@2.7.3(@types/node@24.10.4)(typescript@5.9.3))(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
@@ -23777,9 +23771,9 @@ snapshots:
'@types/filesystem': 0.0.36
'@types/har-format': 1.2.16
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.1(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
'@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.1(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- vite