feat(analytics): track chat browser touchpoints (#3269)

This commit is contained in:
Steven Enamakel
2026-06-03 00:50:24 -04:00
committed by GitHub
parent 2bb692d9af
commit b04b4c84ad
16 changed files with 172 additions and 7 deletions
@@ -49,7 +49,9 @@ const AddAccountModal = ({ open, onClose, onPick, connectedProviders }: AddAccou
</h2>
<button
ref={closeBtnRef}
type="button"
onClick={onClose}
data-analytics-id="add-account-modal-close"
className="rounded p-1 text-stone-500 dark:text-neutral-400 hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60"
aria-label={t('common.close')}>
<CloseIcon className="h-5 w-5" />
@@ -65,6 +67,8 @@ const AddAccountModal = ({ open, onClose, onPick, connectedProviders }: AddAccou
available.map(p => (
<button
key={p.id}
type="button"
data-analytics-id={`add-account-provider-${p.id}`}
data-testid={`add-account-provider-${p.id}`}
onClick={() => onPick(p)}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60">
@@ -49,6 +49,7 @@ export default function RespondQueuePanel({
<button
type="button"
onClick={onRefresh}
data-analytics-id="respond-queue-refresh"
className="rounded-lg border border-stone-200 dark:border-neutral-800 px-2 py-1 text-xs text-stone-600 dark:text-neutral-300 hover:bg-stone-50 dark:hover:bg-neutral-800/60">
{t('common.refresh')}
</button>
@@ -77,6 +78,7 @@ export default function RespondQueuePanel({
<button
key={item.id}
type="button"
data-analytics-id={`respond-queue-item-${item.provider}`}
onClick={() => {
if (item.deepLink) {
void openUrl(item.deepLink);
@@ -2,6 +2,7 @@ import debug from 'debug';
import { useEffect, useRef, useState } from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import { trackEvent } from '../../services/analytics';
import {
hideWebviewAccount,
openWebviewAccount,
@@ -254,8 +255,15 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => {
</div>
<button
type="button"
data-analytics-id={`webview-host-retry-${provider}`}
onClick={() => {
log('retry clicked account=%s provider=%s', accountId, provider);
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'retry_browser_load',
provider,
account_status: status ?? 'unknown',
});
retryWebviewAccountLoad(accountId, provider).catch(() => {
// Same contract as the initial open (OPENHUMAN-REACT-K):
// service-layer dispatched error status + breadcrumb; absorbing
@@ -87,6 +87,7 @@ export const ApprovalRequestCard: React.FC<Props> = ({ threadId, approval }) =>
<Button
variant="primary"
size="sm"
data-analytics-id="chat-approval-approve-once"
onClick={() => void decide('approve_once')}
disabled={deciding !== null}>
{deciding === 'approve_once'
@@ -96,6 +97,7 @@ export const ApprovalRequestCard: React.FC<Props> = ({ threadId, approval }) =>
<Button
variant="secondary"
size="sm"
data-analytics-id="chat-approval-approve-always"
onClick={() => void decide('approve_always_for_tool')}
disabled={deciding !== null}
title={t('chat.approval.alwaysAllowHint')}>
@@ -106,6 +108,7 @@ export const ApprovalRequestCard: React.FC<Props> = ({ threadId, approval }) =>
<Button
variant="secondary"
size="sm"
data-analytics-id="chat-approval-deny"
onClick={() => void decide('deny')}
disabled={deciding !== null}>
{deciding === 'deny' ? t('chat.approval.deciding') : t('chat.approval.deny')}
+4
View File
@@ -212,6 +212,7 @@ export default function ArtifactCard({ artifact, onRetry }: ArtifactCardProps) {
{artifact.status === 'ready' && download.state !== 'done' && (
<button
type="button"
data-analytics-id={`chat-artifact-download-${artifact.kind}`}
onClick={handleDownload}
disabled={download.state === 'downloading'}
className="ml-auto rounded-md bg-ocean-500 hover:bg-ocean-600 disabled:bg-stone-300 dark:disabled:bg-neutral-700 text-white text-xs font-medium px-3 py-1.5 transition-colors">
@@ -223,6 +224,7 @@ export default function ArtifactCard({ artifact, onRetry }: ArtifactCardProps) {
{artifact.status === 'failed' && onRetry && (
<button
type="button"
data-analytics-id={`chat-artifact-retry-${artifact.kind}`}
onClick={() => onRetry(artifact.artifactId)}
className="ml-auto rounded-md bg-stone-200 dark:bg-neutral-700 hover:bg-stone-300 dark:hover:bg-neutral-600 text-stone-700 dark:text-neutral-200 text-xs font-medium px-3 py-1.5 transition-colors">
{t('chat.artifact.retry')}
@@ -242,6 +244,7 @@ export default function ArtifactCard({ artifact, onRetry }: ArtifactCardProps) {
{artifact.error.length > ERROR_REASON_PREVIEW_CHARS && (
<button
type="button"
data-analytics-id="chat-artifact-error-toggle"
onClick={() => setErrorExpanded(prev => !prev)}
className="mt-1 underline text-coral-700 dark:text-coral-300 hover:text-coral-900 dark:hover:text-coral-100">
{errorExpanded ? t('chat.artifact.show_less') : t('chat.artifact.show_more')}
@@ -256,6 +259,7 @@ export default function ArtifactCard({ artifact, onRetry }: ArtifactCardProps) {
</span>
<button
type="button"
data-analytics-id={`chat-artifact-reveal-${artifact.kind}`}
onClick={handleReveal}
className="ml-auto underline hover:text-sage-900 dark:hover:text-sage-100 transition-colors flex-shrink-0">
{t('chat.artifact.reveal')}
@@ -35,6 +35,7 @@ export default function AttachmentPreview({
</div>
<button
type="button"
data-analytics-id="chat-attachment-remove"
aria-label={t('chat.attachment.remove').replace('{name}', attachment.file.name)}
onClick={() => onRemove(attachment.id)}
disabled={disabled}
+3
View File
@@ -138,6 +138,7 @@ export default function ChatComposer({
{attachmentsEnabled && (
<button
type="button"
data-analytics-id="chat-composer-attach-file"
aria-label={t('composer.attachFile')}
title={t('composer.attachFile')}
onClick={() => fileInputRef.current?.click()}
@@ -163,6 +164,7 @@ export default function ChatComposer({
{/* Voice mode — switches to mic-cloud mode */}
<button
type="button"
data-analytics-id="chat-composer-voice-mode"
aria-label={t('composer.voiceMode')}
title={t('composer.voiceMode')}
onClick={onSwitchToMicCloud}
@@ -187,6 +189,7 @@ export default function ChatComposer({
{/* Send button — always visible */}
<button
type="button"
data-analytics-id="chat-composer-send"
data-testid="send-message-button"
aria-label={t('chat.send')}
title={t('chat.send')}
@@ -39,6 +39,7 @@ export default function ChatFilesChip({ threadId }: ChatFilesChipProps) {
<div className="relative inline-block">
<button
type="button"
data-analytics-id="chat-files-chip"
onClick={() => setOpen(prev => !prev)}
aria-haspopup="dialog"
aria-expanded={open}
@@ -263,6 +263,7 @@ export default function ChatFilesPanel({ threadId, artifacts, onClose }: ChatFil
<button
type="button"
onClick={onClose}
data-analytics-id="chat-files-panel-close"
aria-label={t('chat.files.panel.close')}
className="w-6 h-6 flex items-center justify-center rounded hover:bg-stone-100 dark:hover:bg-neutral-800 text-stone-500 dark:text-neutral-400">
<svg
@@ -307,12 +308,14 @@ export default function ChatFilesPanel({ threadId, artifacts, onClose }: ChatFil
<button
type="button"
onClick={() => setConfirmDeleteId(null)}
data-analytics-id="chat-files-delete-cancel"
className="rounded-md bg-stone-100 dark:bg-neutral-800 hover:bg-stone-200 dark:hover:bg-neutral-700 text-stone-700 dark:text-neutral-200 text-[11px] font-medium px-2 py-1 transition-colors">
{t('chat.files.delete.cancel')}
</button>
<button
type="button"
onClick={() => void handleDeleteConfirm(artifact)}
data-analytics-id={`chat-files-delete-confirm-${artifact.kind}`}
data-testid={`chat-files-confirm-${artifact.artifactId}`}
className="rounded-md bg-coral-500 hover:bg-coral-600 text-white text-[11px] font-medium px-2 py-1 transition-colors">
{t('chat.files.delete.action')}
@@ -324,6 +327,7 @@ export default function ChatFilesPanel({ threadId, artifacts, onClose }: ChatFil
type="button"
onClick={() => void handleDownload(artifact)}
disabled={row.state === 'downloading'}
data-analytics-id={`chat-files-download-${artifact.kind}`}
data-testid={`chat-files-download-${artifact.artifactId}`}
className="rounded-md bg-ocean-500 hover:bg-ocean-600 disabled:bg-stone-300 dark:disabled:bg-neutral-700 text-white text-[11px] font-medium px-2 py-1 transition-colors">
{row.state === 'downloading'
@@ -334,6 +338,7 @@ export default function ChatFilesPanel({ threadId, artifacts, onClose }: ChatFil
<button
type="button"
onClick={() => void handleReveal(row.path!)}
data-analytics-id={`chat-files-reveal-${artifact.kind}`}
data-testid={`chat-files-reveal-${artifact.artifactId}`}
className="text-[11px] underline text-sage-700 dark:text-sage-300 hover:text-sage-900 dark:hover:text-sage-100">
{t('chat.artifact.reveal')}
@@ -342,6 +347,7 @@ export default function ChatFilesPanel({ threadId, artifacts, onClose }: ChatFil
<button
type="button"
onClick={() => setConfirmDeleteId(artifact.artifactId)}
data-analytics-id={`chat-files-delete-${artifact.kind}`}
data-testid={`chat-files-delete-${artifact.artifactId}`}
aria-label={t('chat.files.delete.aria').replace('{title}', artifact.title)}
className="ml-auto rounded-md bg-transparent text-stone-500 dark:text-neutral-400 hover:bg-coral-50 dark:hover:bg-coral-900/20 hover:text-coral-700 dark:hover:text-coral-300 text-[11px] font-medium px-2 py-1 transition-colors">
@@ -14,6 +14,7 @@ export default function ModelQualityPill({ className }: ModelQualityPillProps) {
return (
<button
type="button"
data-analytics-id="chat-model-quality-pill"
aria-label={t('composer.modelSelector')}
title={t('composer.modelSelector')}
disabled
@@ -84,6 +84,7 @@ const TokenUsagePill = () => {
{showPlanPill ? (
<button
type="button"
data-analytics-id="chat-token-plan-pill"
onClick={() => {
void openUrl(BILLING_DASHBOARD_URL);
}}
@@ -78,12 +78,22 @@ export const UnsubscribeApprovalCard: React.FC<Props> = ({ payload }) => {
{status === 'pending' && (
<div className="flex gap-2 mt-4">
<Button variant="primary" size="sm" onClick={handleApprove} disabled={isProcessing}>
<Button
variant="primary"
size="sm"
data-analytics-id="chat-unsubscribe-approve"
onClick={handleApprove}
disabled={isProcessing}>
{isProcessing
? t('chat.unsubscribeApproval.processing')
: t('chat.unsubscribeApproval.approve')}
</Button>
<Button variant="secondary" size="sm" onClick={handleDeny} disabled={isProcessing}>
<Button
variant="secondary"
size="sm"
data-analytics-id="chat-unsubscribe-deny"
onClick={handleDeny}
disabled={isProcessing}>
{t('chat.unsubscribeApproval.deny')}
</Button>
</div>
+66 -5
View File
@@ -42,6 +42,7 @@ interface RailButtonProps {
onClick: () => void;
onContextMenu?: (e: React.MouseEvent) => void;
tooltip: string;
analyticsId: string;
badge?: number;
children: React.ReactNode;
}
@@ -51,12 +52,15 @@ const RailButton = ({
onClick,
onContextMenu,
tooltip,
analyticsId,
badge,
children,
}: RailButtonProps) => (
<button
type="button"
onClick={onClick}
onContextMenu={onContextMenu}
data-analytics-id={analyticsId}
// Issue #1284 — `hover:z-50` lifts the entire button (and its tooltip
// child) above sibling rail buttons during hover. Without it, the
// `hover:scale-105` transform on a non-active button establishes its
@@ -183,8 +187,24 @@ const Accounts = () => {
dispatch(setLastActiveAccount(id));
};
const selectAgent = () => dispatch(setActiveAccount(AGENT_ID));
const selectAgent = () => {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'select_agent',
provider: 'agent',
});
dispatch(setActiveAccount(AGENT_ID));
};
const selectAccount = (id: string) => {
const account = accountsById[id];
if (account) {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'select_account',
provider: account.provider,
account_status: account.status ?? 'unknown',
});
}
dispatch(setActiveAccount(id));
dispatch(setLastActiveAccount(id));
};
@@ -196,6 +216,15 @@ const Accounts = () => {
const handleLogout = async (accountId: string) => {
setCtxMenu(null);
const account = accountsById[accountId];
if (account) {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'disconnect_account',
provider: account.provider,
account_status: account.status ?? 'unknown',
});
}
try {
await purgeWebviewAccount(accountId);
} catch {
@@ -221,10 +250,17 @@ const Accounts = () => {
}, [ctxMenu]);
return (
<div className="relative flex h-full gap-3 overflow-hidden" data-testid="accounts-page">
<div
className="relative flex h-full gap-3 overflow-hidden"
data-testid="accounts-page"
data-analytics-id="chat-right-sidebar">
{/* Narrow icon rail — always rendered. */}
<aside className="z-30 flex w-16 flex-none flex-col items-center gap-2 bg-white/60 dark:bg-neutral-900/60 py-3 backdrop-blur-md my-3 ml-3 rounded-2xl border border-stone-200/70 dark:border-neutral-800/70 shadow-soft">
<RailButton active={isAgentSelected} onClick={selectAgent} tooltip={t('accounts.agent')}>
<RailButton
active={isAgentSelected}
onClick={selectAgent}
tooltip={t('accounts.agent')}
analyticsId="chat-right-sidebar-agent">
<AgentIcon className="h-9 w-9 rounded-lg bg-white dark:bg-neutral-200" />
</RailButton>
@@ -235,13 +271,23 @@ const Accounts = () => {
onClick={() => selectAccount(acct.id)}
onContextMenu={e => openContextMenu(acct.id, e)}
tooltip={acct.label}
analyticsId={`chat-right-sidebar-account-${acct.provider}`}
badge={unreadByAccount[acct.id]}>
<ProviderIcon provider={acct.provider} className="h-8 w-8 rounded-md" />
</RailButton>
))}
<button
onClick={() => setAddOpen(true)}
type="button"
onClick={() => {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'open_add_account',
provider: 'none',
});
setAddOpen(true);
}}
data-analytics-id="chat-right-sidebar-add-account"
data-testid="accounts-add-button"
className="group relative mt-2 flex h-11 w-11 items-center justify-center rounded-xl border border-dashed border-stone-300 dark:border-neutral-700 text-stone-400 dark:text-neutral-500 hover:z-50 hover:bg-stone-50 dark:hover:bg-neutral-800/60 hover:text-stone-600 dark:hover:text-neutral-300"
aria-label={t('accounts.addAccount')}>
@@ -267,7 +313,15 @@ const Accounts = () => {
layout when the panel opens/closes. */}
<button
type="button"
onClick={() => setRespondQueueOpen(prev => !prev)}
onClick={() => {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: respondQueueOpen ? 'hide_respond_queue' : 'show_respond_queue',
provider: 'agent',
});
setRespondQueueOpen(prev => !prev);
}}
data-analytics-id="chat-right-sidebar-respond-queue-toggle"
className="absolute right-4 top-4 z-10 inline-flex items-center gap-1 rounded-full border border-stone-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-1 text-[11px] font-medium text-stone-600 dark:text-neutral-300 shadow-soft hover:bg-stone-50 dark:hover:bg-neutral-800/60"
title={
respondQueueOpen
@@ -296,6 +350,11 @@ const Accounts = () => {
status={respondQueueStatus}
error={respondQueueError}
onRefresh={() => {
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'refresh_respond_queue',
provider: 'agent',
});
void dispatch(fetchRespondQueue());
}}
/>
@@ -325,6 +384,8 @@ const Accounts = () => {
style={{ left: ctxMenu.x, top: ctxMenu.y }}
onMouseDown={e => e.stopPropagation()}>
<button
type="button"
data-analytics-id="chat-right-sidebar-disconnect-account"
onClick={() => void handleLogout(ctxMenu.accountId)}
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-coral-600 hover:bg-stone-100 dark:hover:bg-neutral-800/60">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+39
View File
@@ -1302,6 +1302,7 @@ const Conversations = ({
</h2>
<button
data-testid="new-thread-sidebar-button"
data-analytics-id="chat-sidebar-new-thread"
onClick={() => void handleCreateNewThread()}
className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60 text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors"
title={t('chat.newThread')}>
@@ -1337,6 +1338,7 @@ const Conversations = ({
<div
key={thread.id}
data-testid={`thread-row-${thread.id}`}
data-analytics-id="chat-sidebar-thread-row"
role="button"
tabIndex={0}
onClick={() => {
@@ -1366,6 +1368,8 @@ const Conversations = ({
{resolveThreadDisplayTitle(thread.id)}
</p>
<button
type="button"
data-analytics-id="chat-sidebar-delete-thread"
onClick={e => {
e.stopPropagation();
setDeleteModal({
@@ -1433,6 +1437,8 @@ const Conversations = ({
className="flex items-center gap-2 px-4 py-2.5 border-b border-stone-100 dark:border-neutral-800"
data-walkthrough="chat-agent-panel">
<button
type="button"
data-analytics-id="chat-header-toggle-sidebar"
onClick={() => setShowSidebar(prev => !prev)}
className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60 text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors"
title={effectiveShowSidebar ? t('chat.hideSidebar') : t('chat.showSidebar')}>
@@ -1449,6 +1455,7 @@ const Conversations = ({
{selectedThreadParent ? (
<button
type="button"
data-analytics-id="chat-header-back-to-parent-thread"
onClick={() => {
dispatch(setSelectedThread(selectedThreadParent.id));
void dispatch(loadThreadMessages(selectedThreadParent.id));
@@ -1487,6 +1494,7 @@ const Conversations = ({
{selectedThreadId && (
<button
type="button"
data-analytics-id="chat-header-edit-thread-title"
onClick={handleStartEditTitle}
aria-label={t('chat.editThreadTitle')}
title={t('chat.editThreadTitle')}
@@ -1523,6 +1531,7 @@ const Conversations = ({
</select>
<button
type="button"
data-analytics-id="chat-header-create-agent-profile-toggle"
onClick={() => setProfileDraftOpen(prev => !prev)}
className="h-7 w-7 rounded-lg text-xs font-medium text-stone-500 dark:text-neutral-400 transition-colors hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200"
title={t('chat.agentProfile.create')}
@@ -1535,7 +1544,9 @@ const Conversations = ({
)}
<TokenUsagePill />
<button
type="button"
data-testid="new-thread-button"
data-analytics-id="chat-header-new-thread"
onClick={() => void handleCreateNewThread()}
className="px-2.5 py-1 rounded-lg text-xs font-medium text-primary-600 hover:bg-primary-50 transition-colors"
title={t('chat.newThreadShortcut')}>
@@ -1586,6 +1597,7 @@ const Conversations = ({
/>
<button
type="button"
data-analytics-id="chat-agent-profile-save"
onClick={() => void handleCreateAgentProfile()}
disabled={!profileDraft.name.trim()}
className="h-8 rounded-lg bg-primary-500 px-3 text-xs font-medium text-white transition-colors hover:bg-primary-600 disabled:opacity-40">
@@ -1593,6 +1605,7 @@ const Conversations = ({
</button>
<button
type="button"
data-analytics-id="chat-agent-profile-cancel"
onClick={() => {
setProfileDraft(DEFAULT_PROFILE_DRAFT);
setProfileDraftOpen(false);
@@ -1639,6 +1652,8 @@ const Conversations = ({
{messagesError}
</p>
<button
type="button"
data-analytics-id="chat-messages-reload"
onClick={() => window.location.reload()}
className="text-xs text-primary-400 hover:text-primary-300 transition-colors">
{t('common.reload')}
@@ -1764,6 +1779,8 @@ const Conversations = ({
</div>
)}
<button
type="button"
data-analytics-id="chat-message-copy"
onClick={() => handleCopyMessage(msg.id, msg.content)}
className={`absolute -top-1 ${msg.sender === 'user' ? '-left-8' : '-right-8'} p-1 rounded-md opacity-0 group-hover/msg:opacity-100 hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800 text-stone-400 dark:text-neutral-500 hover:text-stone-600 dark:hover:text-neutral-300 transition-all`}
title={t('chat.copyResponse')}>
@@ -1807,6 +1824,8 @@ const Conversations = ({
{myReactions.map(emoji => (
<button
key={emoji}
type="button"
data-analytics-id="chat-message-reaction-remove"
onClick={() =>
selectedThreadId &&
void dispatch(
@@ -1828,6 +1847,8 @@ const Conversations = ({
{['👍', '❤️', '😂', '🔥', '👀', '🎯'].map(emoji => (
<button
key={emoji}
type="button"
data-analytics-id="chat-message-reaction-pick"
onClick={() => {
if (selectedThreadId) {
void dispatch(
@@ -1846,6 +1867,8 @@ const Conversations = ({
</button>
))}
<button
type="button"
data-analytics-id="chat-message-reaction-close"
onClick={() => setReactionPickerMsgId(null)}
className="ml-0.5 text-stone-600 dark:text-neutral-300 hover:text-stone-400 dark:hover:text-neutral-500 text-xs px-0.5">
@@ -1853,6 +1876,8 @@ const Conversations = ({
</div>
) : (
<button
type="button"
data-analytics-id="chat-message-reaction-open"
onClick={() => setReactionPickerMsgId(msg.id)}
className="opacity-0 group-hover/msg:opacity-100 flex items-center px-1.5 py-0.5 rounded-full bg-stone-50 dark:bg-neutral-800/60 hover:bg-stone-200 dark:bg-neutral-800 dark:hover:bg-neutral-800 text-stone-500 dark:text-neutral-400 hover:text-stone-300 dark:hover:text-neutral-600 text-xs transition-all"
title={t('chat.addReaction')}>
@@ -1968,6 +1993,8 @@ const Conversations = ({
{isSending && rustChat && (
<div className="flex justify-start px-1">
<button
type="button"
data-analytics-id="chat-cancel-generation"
onClick={() => {
if (selectedThreadId) void chatCancel(selectedThreadId);
}}
@@ -2030,6 +2057,8 @@ const Conversations = ({
</p>
</div>
<button
type="button"
data-analytics-id="chat-budget-top-up"
onClick={() => {
void openUrl(BILLING_DASHBOARD_URL);
}}
@@ -2048,6 +2077,8 @@ const Conversations = ({
{sendAdvisory}
</p>
<button
type="button"
data-analytics-id="chat-send-advisory-dismiss"
onClick={() => setSendAdvisory(null)}
className="text-xs text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors ml-2">
{t('common.dismiss')}
@@ -2061,6 +2092,8 @@ const Conversations = ({
{attachError.message}
</p>
<button
type="button"
data-analytics-id="chat-attach-error-dismiss"
onClick={() => setAttachError(null)}
className="text-xs text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 transition-colors ml-2">
{t('common.dismiss')}
@@ -2079,6 +2112,8 @@ const Conversations = ({
sendError.code === 'tts_not_ready' ||
sendError.code === 'voice_synthesis') && (
<button
type="button"
data-analytics-id="chat-send-error-setup"
onClick={() => {
setSendError(null);
// STT/TTS provider settings live on the Voice panel
@@ -2091,6 +2126,8 @@ const Conversations = ({
</button>
)}
<button
type="button"
data-analytics-id="chat-send-error-dismiss"
onClick={() => setSendError(null)}
className="text-xs text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors">
{t('common.dismiss')}
@@ -2178,6 +2215,7 @@ const Conversations = ({
<div className="flex items-center gap-2">
<button
type="button"
data-analytics-id="chat-voice-switch-to-text"
onClick={() => setInputMode('text')}
disabled={isRecording || isTranscribing}
className="w-10 h-10 flex items-center justify-center rounded-full border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 hover:border-stone-300 dark:hover:border-neutral-700 transition-colors disabled:opacity-40"
@@ -2193,6 +2231,7 @@ const Conversations = ({
</button>
<button
type="button"
data-analytics-id="chat-voice-record-toggle"
onClick={() => {
void handleVoiceRecordToggle();
}}
@@ -577,6 +577,26 @@ describe('trackEvent (OpenPanel)', () => {
expect(openPanelPayload().payload.properties.__timestamp).toEqual(expect.any(String));
});
test('allows dedicated Tauri browser click events with provider-level metadata', async () => {
const { initGA, trackEvent } = await freshAnalytics();
initGA();
trackEvent('tauri_browser_click', {
surface: 'chat_right_sidebar',
action: 'select_account',
provider: 'slack',
account_status: 'open',
});
expect(openPanelPayload().payload.name).toBe('tauri_browser_click');
expect(openPanelPayload().payload.properties).toMatchObject({
surface: 'chat_right_sidebar',
action: 'select_account',
provider: 'slack',
account_status: 'open',
user_id: '',
});
});
test('redacts user_id from event debug logs', async () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => undefined);
hoisted.currentUserId = 'user-123';
+1
View File
@@ -103,6 +103,7 @@ export const ALLOWED_EVENTS = new Set([
'skill_install',
'skill_uninstall',
'tab_bar_change',
'tauri_browser_click',
'ui_click',
'ui_control_change',
'ui_form_submit',