Skip to content

Commit 0058a0a

Browse files
committed
🤖 feat: enhance telemetry with runtime, platform, and usage tracking
Add comprehensive telemetry enhancements for better product insights: Runtime & Platform Data: - Add runtimeType (local/worktree/ssh) to workspace_created and message_sent - Add frontend platform info (userAgent, platform) for mux server mode - Enhance backend platform data with nodeVersion and bunVersion New Events: - stream_completed: Track completion vs interruption, duration, output tokens - provider_configured: Track which providers users set up (not keys!) - command_used: Track slash command usage patterns - voice_transcription: Track voice input adoption and success rate Enhanced Events: - app_started: Add vimModeEnabled for vim mode adoption - message_sent: Add thinkingLevel for extended thinking usage All metrics use base-2 rounding for privacy-preserving numerical data. _Generated with `mux`_
1 parent 4d61f89 commit 0058a0a

File tree

15 files changed

+505
-33
lines changed

15 files changed

+505
-33
lines changed

docs/system-prompt.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,4 @@ You are in a git worktree at ${workspacePath}
6262
}
6363
```
6464

65-
6665
{/* END SYSTEM_PROMPT_DOCS */}

src/browser/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { isWorkspaceForkSwitchEvent } from "./utils/workspaceEvents";
3333
import { getThinkingLevelKey } from "@/common/constants/storage";
3434
import type { BranchListResult } from "@/common/orpc/types";
3535
import { useTelemetry } from "./hooks/useTelemetry";
36+
import { getRuntimeTypeForTelemetry } from "@/common/telemetry";
3637
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
3738
import { useAPI } from "@/browser/contexts/API";
3839
import { AuthTokenModal } from "@/browser/components/AuthTokenModal";
@@ -640,7 +641,10 @@ function AppInner() {
640641
});
641642

642643
// Track telemetry
643-
telemetry.workspaceCreated(metadata.id);
644+
telemetry.workspaceCreated(
645+
metadata.id,
646+
getRuntimeTypeForTelemetry(metadata.runtimeConfig)
647+
);
644648

645649
// Clear pending state
646650
clearPendingWorkspaceCreation();

src/browser/components/AIView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { WorkspaceHeader } from "./WorkspaceHeader";
3333
import { getModelName } from "@/common/utils/ai/models";
3434
import type { DisplayedMessage } from "@/common/types/message";
3535
import type { RuntimeConfig } from "@/common/types/runtime";
36+
import { getRuntimeTypeForTelemetry } from "@/common/telemetry";
3637
import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds";
3738
import { evictModelFromLRU } from "@/browser/hooks/useModelLRU";
3839
import { QueuedMessage } from "./Messages/QueuedMessage";
@@ -620,6 +621,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
620621
<ChatInput
621622
variant="workspace"
622623
workspaceId={workspaceId}
624+
runtimeType={getRuntimeTypeForTelemetry(runtimeConfig)}
623625
onMessageSent={handleMessageSent}
624626
onTruncateHistory={handleClearHistory}
625627
onProviderConfig={handleProviderConfig}

src/browser/components/ChatInput/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
119119
const editingMessage = variant === "workspace" ? props.editingMessage : undefined;
120120
const isCompacting = variant === "workspace" ? (props.isCompacting ?? false) : false;
121121
const canInterrupt = variant === "workspace" ? (props.canInterrupt ?? false) : false;
122+
// runtimeType for telemetry - defaults to "worktree" if not provided
123+
const runtimeType = variant === "workspace" ? (props.runtimeType ?? "worktree") : "worktree";
122124

123125
// Storage keys differ by variant
124126
const storageKeys = (() => {
@@ -1015,7 +1017,13 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10151017
setImageAttachments(previousImageAttachments);
10161018
} else {
10171019
// Track telemetry for successful message send
1018-
telemetry.messageSent(sendMessageOptions.model, mode, actualMessageText.length);
1020+
telemetry.messageSent(
1021+
sendMessageOptions.model,
1022+
mode,
1023+
actualMessageText.length,
1024+
runtimeType,
1025+
sendMessageOptions.thinkingLevel ?? "off"
1026+
);
10191027

10201028
// Exit editing mode if we were editing
10211029
if (editingMessage && props.onCancelEdit) {

src/browser/components/ChatInput/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ImagePart } from "@/common/orpc/types";
22
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
3+
import type { TelemetryRuntimeType } from "@/common/telemetry/payload";
34
import type { AutoCompactionCheckResult } from "@/browser/utils/compaction/autoCompactionCheck";
45

56
export interface ChatInputAPI {
@@ -14,6 +15,8 @@ export interface ChatInputAPI {
1415
export interface ChatInputWorkspaceVariant {
1516
variant: "workspace";
1617
workspaceId: string;
18+
/** Runtime type for the workspace (for telemetry) - no sensitive details like SSH host */
19+
runtimeType?: TelemetryRuntimeType;
1720
onMessageSent?: () => void;
1821
onTruncateHistory: (percentage?: number) => Promise<void>;
1922
onProviderConfig?: (provider: string, keyPath: string[], value: string) => Promise<void>;

src/browser/hooks/useTelemetry.ts

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { useCallback } from "react";
2-
import { trackEvent, roundToBase2 } from "@/common/telemetry";
3-
import type { ErrorContext } from "@/common/telemetry/payload";
2+
import { trackEvent, roundToBase2, getFrontendPlatformInfo } from "@/common/telemetry";
3+
import type {
4+
ErrorContext,
5+
TelemetryRuntimeType,
6+
TelemetryThinkingLevel,
7+
TelemetryCommandType,
8+
} from "@/common/telemetry/payload";
49

510
/**
611
* Hook for clean telemetry integration in React components
@@ -16,11 +21,23 @@ import type { ErrorContext } from "@/common/telemetry/payload";
1621
* // Track workspace switch
1722
* telemetry.workspaceSwitched(fromId, toId);
1823
*
19-
* // Track workspace creation
20-
* telemetry.workspaceCreated(workspaceId);
24+
* // Track workspace creation (runtimeType: 'local' | 'worktree' | 'ssh')
25+
* telemetry.workspaceCreated(workspaceId, runtimeType);
2126
*
2227
* // Track message sent
23-
* telemetry.messageSent(model, mode, messageLength);
28+
* telemetry.messageSent(model, mode, messageLength, runtimeType, thinkingLevel);
29+
*
30+
* // Track stream completion
31+
* telemetry.streamCompleted(model, wasInterrupted, durationSecs, outputTokens);
32+
*
33+
* // Track provider configuration
34+
* telemetry.providerConfigured(provider, keyType);
35+
*
36+
* // Track command usage
37+
* telemetry.commandUsed(commandType);
38+
*
39+
* // Track voice transcription
40+
* telemetry.voiceTranscription(audioDurationSecs, success);
2441
*
2542
* // Track error
2643
* telemetry.errorOccurred(errorType, context);
@@ -38,24 +55,104 @@ export function useTelemetry() {
3855
});
3956
}, []);
4057

41-
const workspaceCreated = useCallback((workspaceId: string) => {
42-
console.debug("[useTelemetry] workspaceCreated called", { workspaceId });
58+
const workspaceCreated = useCallback((workspaceId: string, runtimeType: TelemetryRuntimeType) => {
59+
const frontendPlatform = getFrontendPlatformInfo();
60+
console.debug("[useTelemetry] workspaceCreated called", {
61+
workspaceId,
62+
runtimeType,
63+
frontendPlatform,
64+
});
4365
trackEvent({
4466
event: "workspace_created",
4567
properties: {
4668
workspaceId,
69+
runtimeType,
70+
frontendPlatform,
4771
},
4872
});
4973
}, []);
5074

51-
const messageSent = useCallback((model: string, mode: string, messageLength: number) => {
52-
console.debug("[useTelemetry] messageSent called", { model, mode, messageLength });
53-
trackEvent({
54-
event: "message_sent",
55-
properties: {
75+
const messageSent = useCallback(
76+
(
77+
model: string,
78+
mode: string,
79+
messageLength: number,
80+
runtimeType: TelemetryRuntimeType,
81+
thinkingLevel: TelemetryThinkingLevel
82+
) => {
83+
const frontendPlatform = getFrontendPlatformInfo();
84+
console.debug("[useTelemetry] messageSent called", {
5685
model,
5786
mode,
58-
message_length_b2: roundToBase2(messageLength),
87+
messageLength,
88+
runtimeType,
89+
thinkingLevel,
90+
frontendPlatform,
91+
});
92+
trackEvent({
93+
event: "message_sent",
94+
properties: {
95+
model,
96+
mode,
97+
message_length_b2: roundToBase2(messageLength),
98+
runtimeType,
99+
frontendPlatform,
100+
thinkingLevel,
101+
},
102+
});
103+
},
104+
[]
105+
);
106+
107+
const streamCompleted = useCallback(
108+
(model: string, wasInterrupted: boolean, durationSecs: number, outputTokens: number) => {
109+
console.debug("[useTelemetry] streamCompleted called", {
110+
model,
111+
wasInterrupted,
112+
durationSecs,
113+
outputTokens,
114+
});
115+
trackEvent({
116+
event: "stream_completed",
117+
properties: {
118+
model,
119+
wasInterrupted,
120+
duration_b2: roundToBase2(durationSecs),
121+
output_tokens_b2: roundToBase2(outputTokens),
122+
},
123+
});
124+
},
125+
[]
126+
);
127+
128+
const providerConfigured = useCallback((provider: string, keyType: string) => {
129+
console.debug("[useTelemetry] providerConfigured called", { provider, keyType });
130+
trackEvent({
131+
event: "provider_configured",
132+
properties: {
133+
provider,
134+
keyType,
135+
},
136+
});
137+
}, []);
138+
139+
const commandUsed = useCallback((command: TelemetryCommandType) => {
140+
console.debug("[useTelemetry] commandUsed called", { command });
141+
trackEvent({
142+
event: "command_used",
143+
properties: {
144+
command,
145+
},
146+
});
147+
}, []);
148+
149+
const voiceTranscription = useCallback((audioDurationSecs: number, success: boolean) => {
150+
console.debug("[useTelemetry] voiceTranscription called", { audioDurationSecs, success });
151+
trackEvent({
152+
event: "voice_transcription",
153+
properties: {
154+
audio_duration_b2: roundToBase2(audioDurationSecs),
155+
success,
59156
},
60157
});
61158
}, []);
@@ -75,6 +172,10 @@ export function useTelemetry() {
75172
workspaceSwitched,
76173
workspaceCreated,
77174
messageSent,
175+
streamCompleted,
176+
providerConfigured,
177+
commandUsed,
178+
voiceTranscription,
78179
errorOccurred,
79180
};
80181
}

src/browser/hooks/useVoiceInput.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { useState, useCallback, useRef, useEffect } from "react";
1010
import { matchesKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
1111
import type { APIClient } from "@/browser/contexts/API";
12+
import { trackEvent, roundToBase2 } from "@/common/telemetry";
1213

1314
export type VoiceInputState = "idle" | "recording" | "transcribing";
1415

@@ -117,6 +118,9 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
117118
const shouldSendRef = useRef(false);
118119
const wasCancelledRef = useRef(false);
119120

121+
// Track recording start time for duration telemetry
122+
const recordingStartTimeRef = useRef<number>(0);
123+
120124
// Keep callbacks fresh without recreating functions
121125
const callbacksRef = useRef(options);
122126
useEffect(() => {
@@ -134,6 +138,9 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
134138
const shouldSend = shouldSendRef.current;
135139
shouldSendRef.current = false;
136140

141+
// Calculate recording duration for telemetry
142+
const audioDurationSecs = (Date.now() - recordingStartTimeRef.current) / 1000;
143+
137144
try {
138145
// Encode audio as base64 for IPC transport
139146
const buffer = await audioBlob.arrayBuffer();
@@ -144,18 +151,41 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
144151
const api = callbacksRef.current.api;
145152
if (!api) {
146153
callbacksRef.current.onError?.("Voice API not available");
154+
// Track failed transcription
155+
trackEvent({
156+
event: "voice_transcription",
157+
properties: { audio_duration_b2: roundToBase2(audioDurationSecs), success: false },
158+
});
147159
return;
148160
}
149161

150162
const result = await api.voice.transcribe({ audioBase64: base64 });
151163

152164
if (!result.success) {
153165
callbacksRef.current.onError?.(result.error);
166+
// Track failed transcription
167+
trackEvent({
168+
event: "voice_transcription",
169+
properties: { audio_duration_b2: roundToBase2(audioDurationSecs), success: false },
170+
});
154171
return;
155172
}
156173

157174
const text = result.data.trim();
158-
if (!text) return; // Empty transcription, nothing to do
175+
if (!text) {
176+
// Track empty transcription as success (API worked, just no speech)
177+
trackEvent({
178+
event: "voice_transcription",
179+
properties: { audio_duration_b2: roundToBase2(audioDurationSecs), success: true },
180+
});
181+
return;
182+
}
183+
184+
// Track successful transcription
185+
trackEvent({
186+
event: "voice_transcription",
187+
properties: { audio_duration_b2: roundToBase2(audioDurationSecs), success: true },
188+
});
159189

160190
callbacksRef.current.onTranscript(text);
161191

@@ -166,6 +196,11 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
166196
} catch (err) {
167197
const msg = err instanceof Error ? err.message : String(err);
168198
callbacksRef.current.onError?.(`Transcription failed: ${msg}`);
199+
// Track failed transcription
200+
trackEvent({
201+
event: "voice_transcription",
202+
properties: { audio_duration_b2: roundToBase2(audioDurationSecs), success: false },
203+
});
169204
} finally {
170205
setState("idle");
171206
}
@@ -235,6 +270,7 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
235270
recorderRef.current = recorder;
236271
setMediaRecorder(recorder);
237272
recorder.start();
273+
recordingStartTimeRef.current = Date.now();
238274
setState("recording");
239275
} catch (err) {
240276
const msg = err instanceof Error ? err.message : String(err);

0 commit comments

Comments
 (0)