feat(stage): track conversation product events
Add PostHog product events for manual TTS stop clicks and conversation controls across Web, mobile, and Electron chat surfaces. Document the events in metrics ownership and cover the analytics API with Vitest. Signed-off-by: RainbowBird <git@luoling.moe> Commit-Message-Assisted-by: Codex
This commit is contained in:
@@ -125,6 +125,9 @@
|
||||
| Activation / Retention | `first_model_selected` / `model_switched` | 前端(consciousness store watcher) | `packages/stage-ui/src/stores/analytics/index.ts` | PostHog |
|
||||
| Retention | `character_created` | 前端 | `apps/stage-web/src/pages/settings/characters/components/CharacterDialog.vue` | PostHog |
|
||||
| Retention | `chat_session_started` | 前端 | `packages/stage-ui/src/components/scenarios/chat/components/sessions-drawer.vue` | PostHog |
|
||||
| Conversation controls | `chat_session_selected` | 前端 | `packages/stage-ui/src/components/scenarios/chat/components/sessions-drawer.vue` | PostHog |
|
||||
| Conversation controls | `chat_message_deleted` / `chat_messages_cleared` / `chat_message_retried` | 前端 | `packages/stage-layouts/src/components/Layouts/*InteractiveArea.vue` / `packages/stage-layouts/src/components/Widgets/ChatActionButtons.vue` / `apps/stage-tamagotchi/src/renderer/components/InteractiveArea.vue` | PostHog |
|
||||
| Conversation controls | `tts_stop_clicked` | 前端 | `packages/stage-layouts/src/composables/useStopSpeakingButton.ts` | PostHog |
|
||||
| Churn | `subscription_cancelled`(带 cancellation_reason) | 外部 Stripe 数据源 | PostHog Stripe source connector | Stripe/Postgres |
|
||||
| 老事件 | `provider_card_clicked` / `first_message_sent` | 前端 | `packages/stage-ui/src/composables/use-analytics.ts` | PostHog |
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useStopSpeakingButton } from '@proj-airi/stage-layouts/composables/useStopSpeakingButton'
|
||||
import { ChatHistory, JournalPreviewModal } from '@proj-airi/stage-ui/components'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
import { useBackgroundStore } from '@proj-airi/stage-ui/stores/background'
|
||||
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
@@ -57,7 +58,12 @@ const sendModeLabels = computed<Record<SendMode, string>>(() => ({
|
||||
'ctrl-enter': t('stage.send-mode.ctrl-enter'),
|
||||
'double-enter': t('stage.send-mode.double-enter'),
|
||||
}))
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton()
|
||||
const {
|
||||
trackChatMessageDeleted,
|
||||
trackChatMessageRetried,
|
||||
trackChatMessagesCleared,
|
||||
} = useAnalytics()
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton('electron')
|
||||
|
||||
const latestImageEntries = computed(() => {
|
||||
if (!activeCardId.value)
|
||||
@@ -198,7 +204,13 @@ watch(sendMode, () => {
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryItem[])
|
||||
|
||||
async function handleDeleteMessage(index: number) {
|
||||
const message = messages.value[index]
|
||||
await chatSyncStore.requestDeleteMessage({ index })
|
||||
trackChatMessageDeleted({
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
message_role: message?.role ?? 'unknown',
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -210,6 +222,20 @@ async function handleRetryMessage(index: number) {
|
||||
sessionId: chatSession.activeSessionId,
|
||||
index,
|
||||
})
|
||||
trackChatMessageRetried({
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCleanupMessages() {
|
||||
const messageCount = messages.value.filter(message => message.role !== 'system').length
|
||||
await chatSyncStore.requestCleanup()
|
||||
trackChatMessagesCleared({
|
||||
surface: 'electron',
|
||||
source: 'chat_controls',
|
||||
message_count: messageCount,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -351,7 +377,7 @@ async function handleRetryMessage(index: number) {
|
||||
hover:text="red-500 dark:red-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition-colors transition-transform active:scale-95
|
||||
@click="() => chatSyncStore.requestCleanup()"
|
||||
@click="handleCleanupMessages"
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ChatHistoryItem } from '@proj-airi/stage-ui/types/chat'
|
||||
|
||||
import { ChatHistory } from '@proj-airi/stage-ui/components'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useChatStreamStore } from '@proj-airi/stage-ui/stores/chat/stream-store'
|
||||
@@ -20,9 +21,16 @@ const { streamingMessage } = storeToRefs(useChatStreamStore())
|
||||
|
||||
const isLoading = ref(true)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryItem[])
|
||||
const { trackChatMessageDeleted } = useAnalytics()
|
||||
|
||||
function handleDeleteMessage(index: number) {
|
||||
const message = messages.value[index]
|
||||
messages.value = messages.value.filter((_, messageIndex) => messageIndex !== index)
|
||||
trackChatMessageDeleted({
|
||||
surface: 'web',
|
||||
source: 'history',
|
||||
message_role: message?.role ?? 'unknown',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { useThreeViewControl } from '@proj-airi/stage-ui-three'
|
||||
import { ChatHistory, HearingConfigDialog } from '@proj-airi/stage-ui/components'
|
||||
import { ChatSessionsDrawer } from '@proj-airi/stage-ui/components/scenarios/chat'
|
||||
import { useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAnalytics, useAudioAnalyzer } from '@proj-airi/stage-ui/composables'
|
||||
import { useAudioContext } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useChatOrchestratorStore } from '@proj-airi/stage-ui/stores/chat'
|
||||
import { useChatMaintenanceStore } from '@proj-airi/stage-ui/stores/chat/maintenance'
|
||||
@@ -40,9 +40,26 @@ const { messages } = storeToRefs(chatSession)
|
||||
const { streamingMessage } = storeToRefs(chatStream)
|
||||
const { sending } = storeToRefs(chatOrchestrator)
|
||||
const historyMessages = computed(() => messages.value as unknown as ChatHistoryItem[])
|
||||
const { trackChatMessageDeleted, trackChatMessagesCleared } = useAnalytics()
|
||||
|
||||
function handleDeleteMessage(index: number) {
|
||||
const message = messages.value[index]
|
||||
messages.value = messages.value.filter((_, messageIndex) => messageIndex !== index)
|
||||
trackChatMessageDeleted({
|
||||
surface: 'mobile',
|
||||
source: 'history',
|
||||
message_role: message?.role ?? 'unknown',
|
||||
})
|
||||
}
|
||||
|
||||
function handleCleanupMessages() {
|
||||
const messageCount = messages.value.filter(message => message.role !== 'system').length
|
||||
cleanupMessages()
|
||||
trackChatMessagesCleared({
|
||||
surface: 'mobile',
|
||||
source: 'chat_controls',
|
||||
message_count: messageCount,
|
||||
})
|
||||
}
|
||||
|
||||
const messageInput = ref('')
|
||||
@@ -77,7 +94,7 @@ const { isListening, startStreamingTranscription, stopStreamingTranscription } =
|
||||
isStageTamagotchi,
|
||||
},
|
||||
)
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton()
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton('mobile')
|
||||
const toggleTranscription = () => isListening.value ? stopStreamingTranscription() : startStreamingTranscription()
|
||||
|
||||
async function handleSubmit() {
|
||||
@@ -232,7 +249,7 @@ onMounted(() => {
|
||||
bg="neutral-50/70 dark:neutral-800/70"
|
||||
w-fit flex items-center self-end justify-center rounded-xl p-2 backdrop-blur-md
|
||||
title="Cleanup Messages"
|
||||
@click="cleanupMessages()"
|
||||
@click="handleCleanupMessages"
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
import { useChatMaintenanceStore } from '@proj-airi/stage-ui/stores/chat/maintenance'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
|
||||
@@ -8,9 +11,21 @@ import ViewControls from '../Layouts/InteractiveArea/Actions/ViewControls.vue'
|
||||
import { BackgroundDialogPicker } from '../Backgrounds'
|
||||
|
||||
const { cleanupMessages } = useChatMaintenanceStore()
|
||||
const { messages } = storeToRefs(useChatSessionStore())
|
||||
const { trackChatMessagesCleared } = useAnalytics()
|
||||
const { isDark, toggleDark } = useTheme()
|
||||
|
||||
const backgroundDialogOpen = ref(false)
|
||||
|
||||
function handleCleanupMessages() {
|
||||
const messageCount = messages.value.filter(message => message.role !== 'system').length
|
||||
cleanupMessages()
|
||||
trackChatMessagesCleared({
|
||||
surface: 'web',
|
||||
source: 'chat_controls',
|
||||
message_count: messageCount,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,7 +39,7 @@ const backgroundDialogOpen = ref(false)
|
||||
hover:text="red-500 dark:red-400"
|
||||
flex items-center justify-center rounded-md p-2 outline-none
|
||||
transition-colors transition-transform active:scale-95
|
||||
@click="cleanupMessages()"
|
||||
@click="handleCleanupMessages"
|
||||
>
|
||||
<div class="i-solar:trash-bin-2-bold-duotone" />
|
||||
</button>
|
||||
|
||||
@@ -60,7 +60,7 @@ const { isListening, startStreamingTranscription, stopStreamingTranscription, au
|
||||
isStageTamagotchi,
|
||||
},
|
||||
)
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton()
|
||||
const { showStopSpeakingButton, stopSpeakingFromChat } = useStopSpeakingButton('web')
|
||||
|
||||
async function handleSend() {
|
||||
if (!messageInput.value.trim() || isComposing.value) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useStopSpeakingButton } from './useStopSpeakingButton'
|
||||
|
||||
const nowSpeaking = ref(false)
|
||||
const requestStopSpeakingMock = vi.fn()
|
||||
const trackTtsStopClickedMock = vi.fn()
|
||||
|
||||
vi.mock('@proj-airi/stage-ui/stores/audio', () => ({
|
||||
useSpeakingStore: () => ({
|
||||
@@ -18,6 +19,12 @@ vi.mock('@proj-airi/stage-ui/stores/speech-output-control', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@proj-airi/stage-ui/composables/use-analytics', () => ({
|
||||
useAnalytics: () => ({
|
||||
trackTtsStopClicked: trackTtsStopClickedMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('pinia', () => ({
|
||||
storeToRefs: (store: object) => store,
|
||||
}))
|
||||
@@ -26,7 +33,7 @@ describe('useStopSpeakingButton', () => {
|
||||
it('shows the manual stop button only while the assistant is speaking', () => {
|
||||
nowSpeaking.value = false
|
||||
|
||||
const { showStopSpeakingButton } = useStopSpeakingButton()
|
||||
const { showStopSpeakingButton } = useStopSpeakingButton('web')
|
||||
|
||||
expect(showStopSpeakingButton.value).toBe(false)
|
||||
|
||||
@@ -37,11 +44,16 @@ describe('useStopSpeakingButton', () => {
|
||||
|
||||
it('requests a manual chat stop without touching chat input state', () => {
|
||||
requestStopSpeakingMock.mockClear()
|
||||
trackTtsStopClickedMock.mockClear()
|
||||
|
||||
const { stopSpeakingFromChat } = useStopSpeakingButton()
|
||||
const { stopSpeakingFromChat } = useStopSpeakingButton('mobile')
|
||||
|
||||
stopSpeakingFromChat()
|
||||
|
||||
expect(requestStopSpeakingMock).toHaveBeenCalledWith('manual-chat')
|
||||
expect(trackTtsStopClickedMock).toHaveBeenCalledWith({
|
||||
surface: 'mobile',
|
||||
reason: 'manual-chat',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ConversationAnalyticsSurface } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
import { useSpeakingStore } from '@proj-airi/stage-ui/stores/audio'
|
||||
import { useSpeechOutputControlStore } from '@proj-airi/stage-ui/stores/speech-output-control'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -15,13 +18,15 @@ import { computed } from 'vue'
|
||||
* Returns:
|
||||
* - Visibility state for the button and a click handler for manual chat stops.
|
||||
*/
|
||||
export function useStopSpeakingButton() {
|
||||
export function useStopSpeakingButton(surface: ConversationAnalyticsSurface) {
|
||||
const { nowSpeaking } = storeToRefs(useSpeakingStore())
|
||||
const speechOutputControlStore = useSpeechOutputControlStore()
|
||||
const { trackTtsStopClicked } = useAnalytics()
|
||||
|
||||
const showStopSpeakingButton = computed(() => nowSpeaking.value)
|
||||
|
||||
function stopSpeakingFromChat() {
|
||||
trackTtsStopClicked({ surface, reason: 'manual-chat' })
|
||||
speechOutputControlStore.requestStopSpeaking('manual-chat')
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const { sessionMetas, sessionMessages, activeSessionId } = storeToRefs(chatSessi
|
||||
const { activeCardId } = storeToRefs(useAiriCardStore())
|
||||
const { userId } = storeToRefs(useAuthStore())
|
||||
const { activeModel } = storeToRefs(useConsciousnessStore())
|
||||
const { trackChatSessionStarted } = useAnalytics()
|
||||
const { trackChatSessionSelected, trackChatSessionStarted } = useAnalytics()
|
||||
|
||||
// Re-entry guard for the "new session" button. Without this, a rapid
|
||||
// double-click would call `createSession` twice (creating two orphan
|
||||
@@ -154,6 +154,15 @@ const rows = computed<SessionRow[]>(() => {
|
||||
})
|
||||
|
||||
async function selectSession(sessionId: string) {
|
||||
const selectedRow = rows.value.find(row => row.meta.sessionId === sessionId)
|
||||
if (sessionId !== activeSessionId.value && selectedRow) {
|
||||
trackChatSessionSelected({
|
||||
surface: isDesktop.value ? 'web' : 'mobile',
|
||||
source: 'sessions_drawer',
|
||||
message_count: (sessionMessages.value[sessionId] ?? []).filter(message => message.role !== 'system').length,
|
||||
cloud_synced: !!selectedRow.meta.cloudChatId,
|
||||
})
|
||||
}
|
||||
chatSession.setActiveSession(sessionId)
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useAnalytics } from './use-analytics'
|
||||
|
||||
const analyticsMocks = vi.hoisted(() => ({
|
||||
ensurePosthogInitializedMock: vi.fn(() => true),
|
||||
isPosthogAvailableInBuildMock: vi.fn(() => true),
|
||||
markFirstMessageTrackedMock: vi.fn(),
|
||||
posthogCaptureMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('posthog-js', () => ({
|
||||
default: {
|
||||
capture: analyticsMocks.posthogCaptureMock,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
locale: ref('en'),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/analytics', () => ({
|
||||
useSharedAnalyticsStore: () => ({
|
||||
appStartTime: null,
|
||||
firstMessageTracked: false,
|
||||
markFirstMessageTracked: analyticsMocks.markFirstMessageTrackedMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/analytics/posthog', () => ({
|
||||
ensurePosthogInitialized: analyticsMocks.ensurePosthogInitializedMock,
|
||||
isPosthogAvailableInBuild: analyticsMocks.isPosthogAvailableInBuildMock,
|
||||
}))
|
||||
|
||||
vi.mock('../stores/analytics/privacy-policy', () => ({
|
||||
getAnalyticsPrivacyPolicyUrl: () => 'https://example.com/privacy',
|
||||
}))
|
||||
|
||||
vi.mock('../stores/settings/analytics', () => ({
|
||||
useSettingsAnalytics: () => ({
|
||||
analyticsEnabled: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../stores/settings/general', () => ({
|
||||
useSettingsGeneral: () => ({
|
||||
language: 'en',
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useAnalytics conversation product events', () => {
|
||||
beforeEach(() => {
|
||||
analyticsMocks.posthogCaptureMock.mockClear()
|
||||
analyticsMocks.markFirstMessageTrackedMock.mockClear()
|
||||
analyticsMocks.ensurePosthogInitializedMock.mockClear()
|
||||
analyticsMocks.isPosthogAvailableInBuildMock.mockClear()
|
||||
})
|
||||
|
||||
it('tracks manual TTS stop clicks with the UI surface and reason', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackTtsStopClicked({
|
||||
surface: 'mobile',
|
||||
reason: 'manual-chat',
|
||||
})
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenCalledWith('tts_stop_clicked', {
|
||||
surface: 'mobile',
|
||||
reason: 'manual-chat',
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks chat session selection from the sessions drawer', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackChatSessionSelected({
|
||||
surface: 'web',
|
||||
source: 'sessions_drawer',
|
||||
message_count: 4,
|
||||
cloud_synced: true,
|
||||
})
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenCalledWith('chat_session_selected', {
|
||||
surface: 'web',
|
||||
source: 'sessions_drawer',
|
||||
message_count: 4,
|
||||
cloud_synced: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks destructive and recovery chat message actions', () => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
analytics.trackChatMessageDeleted({
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
message_role: 'assistant',
|
||||
})
|
||||
analytics.trackChatMessagesCleared({
|
||||
surface: 'electron',
|
||||
source: 'chat_controls',
|
||||
message_count: 3,
|
||||
})
|
||||
analytics.trackChatMessageRetried({
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
})
|
||||
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(1, 'chat_message_deleted', {
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
message_role: 'assistant',
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(2, 'chat_messages_cleared', {
|
||||
surface: 'electron',
|
||||
source: 'chat_controls',
|
||||
message_count: 3,
|
||||
})
|
||||
expect(analyticsMocks.posthogCaptureMock).toHaveBeenNthCalledWith(3, 'chat_message_retried', {
|
||||
surface: 'electron',
|
||||
source: 'history',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,16 @@ import { getAnalyticsPrivacyPolicyUrl } from '../stores/analytics/privacy-policy
|
||||
import { useSettingsAnalytics } from '../stores/settings/analytics'
|
||||
import { useSettingsGeneral } from '../stores/settings/general'
|
||||
|
||||
/**
|
||||
* User-facing chat surfaces that can emit product analytics.
|
||||
*/
|
||||
export type ConversationAnalyticsSurface = 'web' | 'mobile' | 'electron'
|
||||
|
||||
/**
|
||||
* Low-cardinality source names for conversation action events.
|
||||
*/
|
||||
export type ConversationAnalyticsSource = 'chat_controls' | 'history' | 'sessions_drawer'
|
||||
|
||||
export function useAnalytics() {
|
||||
const analyticsStore = useSharedAnalyticsStore()
|
||||
const settingsAnalytics = useSettingsAnalytics()
|
||||
@@ -207,6 +217,38 @@ export function useAnalytics() {
|
||||
posthog.capture('message_round', properties)
|
||||
}
|
||||
|
||||
// ─── Conversation action events ─────────────────────────────────────
|
||||
|
||||
function trackTtsStopClicked(properties: { surface: ConversationAnalyticsSurface, reason: 'manual-chat' }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('tts_stop_clicked', properties)
|
||||
}
|
||||
|
||||
function trackChatSessionSelected(properties: { surface: ConversationAnalyticsSurface, source: 'sessions_drawer', message_count: number, cloud_synced: boolean }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('chat_session_selected', properties)
|
||||
}
|
||||
|
||||
function trackChatMessageDeleted(properties: { surface: ConversationAnalyticsSurface, source: 'history', message_role: string }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('chat_message_deleted', properties)
|
||||
}
|
||||
|
||||
function trackChatMessagesCleared(properties: { surface: ConversationAnalyticsSurface, source: 'chat_controls', message_count: number }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('chat_messages_cleared', properties)
|
||||
}
|
||||
|
||||
function trackChatMessageRetried(properties: { surface: ConversationAnalyticsSurface, source: 'history' }) {
|
||||
if (!canCapture())
|
||||
return
|
||||
posthog.capture('chat_message_retried', properties)
|
||||
}
|
||||
|
||||
// ─── STT events ──────────────────────────────────────────────────────
|
||||
|
||||
function trackSttStarted(provider: string) {
|
||||
@@ -362,6 +404,11 @@ export function useAnalytics() {
|
||||
trackLlmFirstToken,
|
||||
trackAssistantResponseRendered,
|
||||
trackMessageRound,
|
||||
trackTtsStopClicked,
|
||||
trackChatSessionSelected,
|
||||
trackChatMessageDeleted,
|
||||
trackChatMessagesCleared,
|
||||
trackChatMessageRetried,
|
||||
|
||||
trackSttStarted,
|
||||
trackSttSucceeded,
|
||||
|
||||
Reference in New Issue
Block a user