Skip to content

Commit 2c91806

Browse files
authored
🤖 feat: move telemetry reporting to backend (#905)
## Summary Moves telemetry event capture from the browser (`posthog-js`) to the Electron main process (`posthog-node`). Events from the renderer are forwarded via ORPC to the backend TelemetryService. ## Problem Ad blockers can intercept and block network requests to PostHog's servers (`us.i.posthog.com`) from the browser/renderer process, causing telemetry to be silently dropped. ## Solution Move telemetry to the main process using `posthog-node`. The main process makes direct HTTP requests that aren't subject to browser-based ad blocking. ### Architecture ```mermaid flowchart LR subgraph Renderer["Renderer (Browser)"] A[useTelemetry hook] B[trackEvent calls] end subgraph Main["Main Process (Node.js)"] C[ORPC telemetry routes] D[posthog-node client] end E[(PostHog)] A --> B B -->|ORPC| C C --> D D -->|HTTP| E ``` ## Changes - **New**: `posthog-node` dependency - **New**: `TelemetryService` in `src/node/services/` - **New**: ORPC `telemetry.{track, setEnabled, isEnabled}` routes - **Updated**: Frontend client now calls backend via ORPC - **Simplified**: Payload types no longer require base properties from frontend ## What Stays the Same - `useTelemetry()` hook API - `trackEvent()` function signature - Opt-out UX via localStorage - All existing event types ## Testing - `make typecheck` ✅ - `make lint` ✅ - `make static-check` ✅ - `make test` (1 pre-existing failure unrelated to this PR) _Generated with `mux`_
1 parent 110b3a0 commit 2c91806

File tree

28 files changed

+361
-376
lines changed

28 files changed

+361
-376
lines changed

bun.lock

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"lockfileVersion": 1,
3-
"configVersion": 0,
43
"workspaces": {
54
"": {
65
"name": "mux",
@@ -50,6 +49,7 @@
5049
"ollama-ai-provider-v2": "^1.5.4",
5150
"openai": "^6.9.1",
5251
"parse-duration": "^2.1.4",
52+
"posthog-node": "^5.17.0",
5353
"rehype-harden": "^1.1.5",
5454
"shescape": "^2.1.6",
5555
"source-map-support": "^0.5.21",
@@ -3003,6 +3003,8 @@
30033003

30043004
"posthog-js": ["posthog-js@1.299.0", "", { "dependencies": { "@posthog/core": "1.6.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" } }, "sha512-euHXKcEqQpRJNWitudVl4/doTJsftgaBDRLNGczt/v3S9N6ppLMzEOmeoqvNhNDIlpxGVlTvSawfw9HeW1r5nA=="],
30053005

3006+
"posthog-node": ["posthog-node@5.17.0", "", { "dependencies": { "@posthog/core": "1.7.0" } }, "sha512-M+ftj0kLJk6wVF1xW5cStSany0LBC6YDVO7RPma2poo+PrpeiTk+ovhqcIqWAySDdTcBHJfBV9aIFYWPl2y6kg=="],
3007+
30063008
"preact": ["preact@10.28.0", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="],
30073009

30083010
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -4085,6 +4087,8 @@
40854087

40864088
"pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
40874089

4090+
"posthog-node/@posthog/core": ["@posthog/core@1.7.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-d6ZV4grpzeH/6/LP8quMVpSjY1puRkrqfwcPvGRKUAX7tb7YHyp/zMiTDuJmOFbpUxAMBXH5nDwcPiyCY2WGzA=="],
4091+
40884092
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
40894093

40904094
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],

docs/telemetry.md

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ mux collects anonymous usage telemetry to help us understand how the product is
44

55
## Privacy Policy
66

7-
- **Opt-out by default**: You can disable telemetry at any time
87
- **No personal information**: We never collect usernames, project names, file paths, or code content
98
- **Random IDs only**: Only randomly-generated workspace IDs are sent (impossible to trace back to you)
109
- **No hashing**: We don't hash sensitive data because hashing is vulnerable to rainbow table attacks
11-
- **Transparent data**: See exactly what data structures we send in [`src/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/telemetry/payload.ts)
10+
- **Transparent data**: See exactly what data structures we send in [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts)
1211

1312
## What We Track
1413

@@ -36,26 +35,19 @@ All telemetry events include basic system information:
3635

3736
## Disabling Telemetry
3837

39-
You can disable telemetry at any time using the `/telemetry` slash command:
38+
To disable telemetry, set the `MUX_DISABLE_TELEMETRY` environment variable before starting the app:
4039

41-
```
42-
/telemetry off
43-
```
44-
45-
To re-enable it:
46-
47-
```
48-
/telemetry on
40+
```bash
41+
MUX_DISABLE_TELEMETRY=1 mux
4942
```
5043

51-
Your preference is saved and persists across app restarts.
44+
This completely disables all telemetry collection at the backend level.
5245

5346
## Source Code
5447

5548
For complete transparency, you can review the telemetry implementation:
5649

57-
- **Payload definitions**: [`src/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/telemetry/payload.ts) - All data structures we send
58-
- **Client code**: [`src/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/telemetry/client.ts) - How telemetry is sent
59-
- **Privacy utilities**: [`src/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/telemetry/utils.ts) - Base-2 rounding and helpers
60-
61-
The telemetry system includes debug logging that you can see in the developer console (View → Toggle Developer Tools).
50+
- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts) - All data structures we send
51+
- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/mux/blob/main/src/node/services/telemetryService.ts) - Server-side telemetry handling
52+
- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/client.ts) - Frontend to backend relay
53+
- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/utils.ts) - Base-2 rounding helper

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"ollama-ai-provider-v2": "^1.5.4",
9191
"openai": "^6.9.1",
9292
"parse-duration": "^2.1.4",
93+
"posthog-node": "^5.17.0",
9394
"rehype-harden": "^1.1.5",
9495
"shescape": "^2.1.6",
9596
"source-map-support": "^0.5.21",

src/browser/components/ChatInput/index.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import type { ThinkingLevel } from "@/common/types/thinking";
6363
import type { MuxFrontendMetadata } from "@/common/types/message";
6464
import { MODEL_ABBREVIATION_EXAMPLES } from "@/common/constants/knownModels";
6565
import { useTelemetry } from "@/browser/hooks/useTelemetry";
66-
import { setTelemetryEnabled } from "@/common/telemetry";
66+
6767
import { getTokenCountPromise } from "@/browser/utils/tokenizer/rendererClient";
6868
import { CreationCenterContent } from "./CreationCenterContent";
6969
import { cn } from "@/common/lib/utils";
@@ -726,18 +726,6 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
726726
return;
727727
}
728728

729-
// Handle /telemetry command
730-
if (parsed.type === "telemetry-set") {
731-
setInput(""); // Clear input immediately
732-
setTelemetryEnabled(parsed.enabled);
733-
setToast({
734-
id: Date.now().toString(),
735-
type: "success",
736-
message: `Telemetry ${parsed.enabled ? "enabled" : "disabled"}`,
737-
});
738-
return;
739-
}
740-
741729
// Handle /compact command
742730
if (parsed.type === "compact") {
743731
if (!api) {

src/browser/components/ChatInputToasts.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,6 @@ export const createCommandToast = (parsed: ParsedCommand): Toast | null => {
8888
),
8989
};
9090

91-
case "telemetry-help":
92-
return {
93-
id: Date.now().toString(),
94-
type: "error",
95-
title: "Telemetry Command",
96-
message: "Enable or disable usage telemetry",
97-
solution: (
98-
<>
99-
<SolutionLabel>Usage:</SolutionLabel>
100-
/telemetry &lt;on|off&gt;
101-
<br />
102-
<br />
103-
<SolutionLabel>Examples:</SolutionLabel>
104-
/telemetry off
105-
<br />
106-
/telemetry on
107-
</>
108-
),
109-
};
110-
11191
case "fork-help":
11292
return {
11393
id: Date.now().toString(),

src/browser/components/TitleBar.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { VERSION } from "@/version";
44
import { SettingsButton } from "./SettingsButton";
55
import { TooltipWrapper, Tooltip } from "./Tooltip";
66
import type { UpdateStatus } from "@/common/orpc/types";
7-
import { isTelemetryEnabled } from "@/common/telemetry";
7+
88
import { useTutorial } from "@/browser/contexts/TutorialContext";
99
import { useAPI } from "@/browser/contexts/API";
1010

@@ -80,7 +80,7 @@ export function TitleBar() {
8080
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>({ type: "idle" });
8181
const [isCheckingOnHover, setIsCheckingOnHover] = useState(false);
8282
const lastHoverCheckTime = useRef<number>(0);
83-
const telemetryEnabled = isTelemetryEnabled();
83+
8484
const { startSequence } = useTutorial();
8585

8686
// Start settings tutorial on first launch
@@ -93,11 +93,6 @@ export function TitleBar() {
9393
}, [startSequence]);
9494

9595
useEffect(() => {
96-
// Skip update checks if telemetry is disabled
97-
if (!telemetryEnabled) {
98-
return;
99-
}
100-
10196
// Skip update checks in browser mode - app updates only apply to Electron
10297
if (!window.api) {
10398
return;
@@ -134,11 +129,9 @@ export function TitleBar() {
134129
controller.abort();
135130
clearInterval(checkInterval);
136131
};
137-
}, [telemetryEnabled, api]);
132+
}, [api]);
138133

139134
const handleIndicatorHover = () => {
140-
if (!telemetryEnabled) return;
141-
142135
// Debounce: Only check once per cooldown period on hover
143136
const now = Date.now();
144137

@@ -161,8 +154,6 @@ export function TitleBar() {
161154
};
162155

163156
const handleUpdateClick = () => {
164-
if (!telemetryEnabled) return; // No-op if telemetry disabled
165-
166157
if (updateStatus.type === "available") {
167158
api?.update.download().catch(console.error);
168159
} else if (updateStatus.type === "downloaded") {
@@ -174,12 +165,7 @@ export function TitleBar() {
174165
const currentVersion = gitDescribe ?? "dev";
175166
const lines: React.ReactNode[] = [`Current: ${currentVersion}`];
176167

177-
if (!telemetryEnabled) {
178-
lines.push(
179-
"Update checks disabled (telemetry is off)",
180-
"Enable telemetry to receive updates."
181-
);
182-
} else if (isCheckingOnHover || updateStatus.type === "checking") {
168+
if (isCheckingOnHover || updateStatus.type === "checking") {
183169
lines.push("Checking for updates...");
184170
} else {
185171
switch (updateStatus.type) {
@@ -224,8 +210,6 @@ export function TitleBar() {
224210
};
225211

226212
const getIndicatorStatus = (): "available" | "downloading" | "downloaded" | "disabled" => {
227-
if (!telemetryEnabled) return "disabled";
228-
229213
if (isCheckingOnHover || updateStatus.type === "checking") return "disabled";
230214

231215
switch (updateStatus.type) {

src/browser/hooks/useTelemetry.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useCallback } from "react";
2-
import { trackEvent, getBaseTelemetryProperties, roundToBase2 } from "@/common/telemetry";
2+
import { trackEvent, roundToBase2 } from "@/common/telemetry";
33
import type { ErrorContext } from "@/common/telemetry/payload";
44

55
/**
66
* Hook for clean telemetry integration in React components
77
*
8-
* Provides type-safe telemetry tracking with base properties automatically included.
8+
* Provides type-safe telemetry tracking. Base properties (version, platform, etc.)
9+
* are automatically added by the backend TelemetryService.
10+
*
911
* Usage:
1012
*
1113
* ```tsx
@@ -30,7 +32,6 @@ export function useTelemetry() {
3032
trackEvent({
3133
event: "workspace_switched",
3234
properties: {
33-
...getBaseTelemetryProperties(),
3435
fromWorkspaceId,
3536
toWorkspaceId,
3637
},
@@ -42,7 +43,6 @@ export function useTelemetry() {
4243
trackEvent({
4344
event: "workspace_created",
4445
properties: {
45-
...getBaseTelemetryProperties(),
4646
workspaceId,
4747
},
4848
});
@@ -53,7 +53,6 @@ export function useTelemetry() {
5353
trackEvent({
5454
event: "message_sent",
5555
properties: {
56-
...getBaseTelemetryProperties(),
5756
model,
5857
mode,
5958
message_length_b2: roundToBase2(messageLength),
@@ -66,7 +65,6 @@ export function useTelemetry() {
6665
trackEvent({
6766
event: "error_occurred",
6867
properties: {
69-
...getBaseTelemetryProperties(),
7068
errorType,
7169
context,
7270
},

src/browser/utils/chatCommands.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { DEFAULT_COMPACTION_WORD_TARGET, WORDS_TO_TOKENS_RATIO } from "@/common/
3232
// ============================================================================
3333

3434
import { createCommandToast } from "@/browser/components/ChatInputToasts";
35-
import { setTelemetryEnabled } from "@/common/telemetry";
3635

3736
export interface ForkOptions {
3837
client: RouterClient<AppRouter>;
@@ -227,17 +226,6 @@ export async function processSlashCommand(
227226
return { clearInput: true, toastShown: false };
228227
}
229228

230-
if (parsed.type === "telemetry-set") {
231-
setInput("");
232-
setTelemetryEnabled(parsed.enabled);
233-
setToast({
234-
id: Date.now().toString(),
235-
type: "success",
236-
message: `Telemetry ${parsed.enabled ? "enabled" : "disabled"}`,
237-
});
238-
return { clearInput: true, toastShown: true };
239-
}
240-
241229
// 2. Workspace Commands
242230
const workspaceCommands = ["clear", "truncate", "compact", "fork", "new"];
243231
const isWorkspaceCommand = workspaceCommands.includes(parsed.type);

src/browser/utils/slashCommands/registry.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -456,46 +456,6 @@ const vimCommandDefinition: SlashCommandDefinition = {
456456
},
457457
};
458458

459-
const telemetryCommandDefinition: SlashCommandDefinition = {
460-
key: "telemetry",
461-
description: "Enable or disable telemetry",
462-
handler: ({ cleanRemainingTokens }): ParsedCommand => {
463-
if (cleanRemainingTokens.length === 0) {
464-
return { type: "telemetry-help" };
465-
}
466-
467-
if (cleanRemainingTokens.length === 1) {
468-
const arg = cleanRemainingTokens[0].toLowerCase();
469-
if (arg === "on" || arg === "off") {
470-
return { type: "telemetry-set", enabled: arg === "on" };
471-
}
472-
}
473-
474-
return {
475-
type: "unknown-command",
476-
command: "telemetry",
477-
subcommand: cleanRemainingTokens[0],
478-
};
479-
},
480-
suggestions: ({ stage, partialToken }) => {
481-
if (stage === 1) {
482-
const options = [
483-
{ key: "on", description: "Enable telemetry" },
484-
{ key: "off", description: "Disable telemetry" },
485-
];
486-
487-
return filterAndMapSuggestions(options, partialToken, (definition) => ({
488-
id: `command:telemetry:${definition.key}`,
489-
display: definition.key,
490-
description: definition.description,
491-
replacement: `/telemetry ${definition.key}`,
492-
}));
493-
}
494-
495-
return null;
496-
},
497-
};
498-
499459
const forkCommandDefinition: SlashCommandDefinition = {
500460
key: "fork",
501461
description:
@@ -631,7 +591,7 @@ export const SLASH_COMMAND_DEFINITIONS: readonly SlashCommandDefinition[] = [
631591
compactCommandDefinition,
632592
modelCommandDefinition,
633593
providersCommandDefinition,
634-
telemetryCommandDefinition,
594+
635595
forkCommandDefinition,
636596
newCommandDefinition,
637597
vimCommandDefinition,

src/browser/utils/slashCommands/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ export type ParsedCommand =
1919
| { type: "clear" }
2020
| { type: "truncate"; percentage: number }
2121
| { type: "compact"; maxOutputTokens?: number; continueMessage?: string; model?: string }
22-
| { type: "telemetry-set"; enabled: boolean }
23-
| { type: "telemetry-help" }
2422
| { type: "fork"; newName: string; startMessage?: string }
2523
| { type: "fork-help" }
2624
| {

0 commit comments

Comments
 (0)