Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "mux",
Expand Down Expand Up @@ -50,6 +49,7 @@
"ollama-ai-provider-v2": "^1.5.4",
"openai": "^6.9.1",
"parse-duration": "^2.1.4",
"posthog-node": "^5.17.0",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
Expand Down Expand Up @@ -3003,6 +3003,8 @@

"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=="],

"posthog-node": ["posthog-node@5.17.0", "", { "dependencies": { "@posthog/core": "1.7.0" } }, "sha512-M+ftj0kLJk6wVF1xW5cStSany0LBC6YDVO7RPma2poo+PrpeiTk+ovhqcIqWAySDdTcBHJfBV9aIFYWPl2y6kg=="],

"preact": ["preact@10.28.0", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="],

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

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

"posthog-node/@posthog/core": ["@posthog/core@1.7.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-d6ZV4grpzeH/6/LP8quMVpSjY1puRkrqfwcPvGRKUAX7tb7YHyp/zMiTDuJmOFbpUxAMBXH5nDwcPiyCY2WGzA=="],

"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],

"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
Expand Down
26 changes: 9 additions & 17 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ mux collects anonymous usage telemetry to help us understand how the product is

## Privacy Policy

- **Opt-out by default**: You can disable telemetry at any time
- **No personal information**: We never collect usernames, project names, file paths, or code content
- **Random IDs only**: Only randomly-generated workspace IDs are sent (impossible to trace back to you)
- **No hashing**: We don't hash sensitive data because hashing is vulnerable to rainbow table attacks
- **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)
- **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)

## What We Track

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

## Disabling Telemetry

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

```
/telemetry off
```

To re-enable it:

```
/telemetry on
```bash
MUX_DISABLE_TELEMETRY=1 mux
```

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

## Source Code

For complete transparency, you can review the telemetry implementation:

- **Payload definitions**: [`src/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/telemetry/payload.ts) - All data structures we send
- **Client code**: [`src/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/telemetry/client.ts) - How telemetry is sent
- **Privacy utilities**: [`src/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/telemetry/utils.ts) - Base-2 rounding and helpers

The telemetry system includes debug logging that you can see in the developer console (View → Toggle Developer Tools).
- **Payload definitions**: [`src/common/telemetry/payload.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/payload.ts) - All data structures we send
- **Backend service**: [`src/node/services/telemetryService.ts`](https://github.com/coder/mux/blob/main/src/node/services/telemetryService.ts) - Server-side telemetry handling
- **Frontend client**: [`src/common/telemetry/client.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/client.ts) - Frontend to backend relay
- **Privacy utilities**: [`src/common/telemetry/utils.ts`](https://github.com/coder/mux/blob/main/src/common/telemetry/utils.ts) - Base-2 rounding helper
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"ollama-ai-provider-v2": "^1.5.4",
"openai": "^6.9.1",
"parse-duration": "^2.1.4",
"posthog-node": "^5.17.0",
"rehype-harden": "^1.1.5",
"shescape": "^2.1.6",
"source-map-support": "^0.5.21",
Expand Down
14 changes: 1 addition & 13 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import type { ThinkingLevel } from "@/common/types/thinking";
import type { MuxFrontendMetadata } from "@/common/types/message";
import { MODEL_ABBREVIATION_EXAMPLES } from "@/common/constants/knownModels";
import { useTelemetry } from "@/browser/hooks/useTelemetry";
import { setTelemetryEnabled } from "@/common/telemetry";

import { getTokenCountPromise } from "@/browser/utils/tokenizer/rendererClient";
import { CreationCenterContent } from "./CreationCenterContent";
import { cn } from "@/common/lib/utils";
Expand Down Expand Up @@ -726,18 +726,6 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
return;
}

// Handle /telemetry command
if (parsed.type === "telemetry-set") {
setInput(""); // Clear input immediately
setTelemetryEnabled(parsed.enabled);
setToast({
id: Date.now().toString(),
type: "success",
message: `Telemetry ${parsed.enabled ? "enabled" : "disabled"}`,
});
return;
}

// Handle /compact command
if (parsed.type === "compact") {
if (!api) {
Expand Down
20 changes: 0 additions & 20 deletions src/browser/components/ChatInputToasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,26 +88,6 @@ export const createCommandToast = (parsed: ParsedCommand): Toast | null => {
),
};

case "telemetry-help":
return {
id: Date.now().toString(),
type: "error",
title: "Telemetry Command",
message: "Enable or disable usage telemetry",
solution: (
<>
<SolutionLabel>Usage:</SolutionLabel>
/telemetry &lt;on|off&gt;
<br />
<br />
<SolutionLabel>Examples:</SolutionLabel>
/telemetry off
<br />
/telemetry on
</>
),
};

case "fork-help":
return {
id: Date.now().toString(),
Expand Down
24 changes: 4 additions & 20 deletions src/browser/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { VERSION } from "@/version";
import { SettingsButton } from "./SettingsButton";
import { TooltipWrapper, Tooltip } from "./Tooltip";
import type { UpdateStatus } from "@/common/orpc/types";
import { isTelemetryEnabled } from "@/common/telemetry";

import { useTutorial } from "@/browser/contexts/TutorialContext";
import { useAPI } from "@/browser/contexts/API";

Expand Down Expand Up @@ -80,7 +80,7 @@ export function TitleBar() {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>({ type: "idle" });
const [isCheckingOnHover, setIsCheckingOnHover] = useState(false);
const lastHoverCheckTime = useRef<number>(0);
const telemetryEnabled = isTelemetryEnabled();

const { startSequence } = useTutorial();

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

useEffect(() => {
// Skip update checks if telemetry is disabled
if (!telemetryEnabled) {
return;
}

// Skip update checks in browser mode - app updates only apply to Electron
if (!window.api) {
return;
Expand Down Expand Up @@ -134,11 +129,9 @@ export function TitleBar() {
controller.abort();
clearInterval(checkInterval);
};
}, [telemetryEnabled, api]);
}, [api]);

const handleIndicatorHover = () => {
if (!telemetryEnabled) return;

// Debounce: Only check once per cooldown period on hover
const now = Date.now();

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

const handleUpdateClick = () => {
if (!telemetryEnabled) return; // No-op if telemetry disabled

if (updateStatus.type === "available") {
api?.update.download().catch(console.error);
} else if (updateStatus.type === "downloaded") {
Expand All @@ -174,12 +165,7 @@ export function TitleBar() {
const currentVersion = gitDescribe ?? "dev";
const lines: React.ReactNode[] = [`Current: ${currentVersion}`];

if (!telemetryEnabled) {
lines.push(
"Update checks disabled (telemetry is off)",
"Enable telemetry to receive updates."
);
} else if (isCheckingOnHover || updateStatus.type === "checking") {
if (isCheckingOnHover || updateStatus.type === "checking") {
lines.push("Checking for updates...");
} else {
switch (updateStatus.type) {
Expand Down Expand Up @@ -224,8 +210,6 @@ export function TitleBar() {
};

const getIndicatorStatus = (): "available" | "downloading" | "downloaded" | "disabled" => {
if (!telemetryEnabled) return "disabled";

if (isCheckingOnHover || updateStatus.type === "checking") return "disabled";

switch (updateStatus.type) {
Expand Down
10 changes: 4 additions & 6 deletions src/browser/hooks/useTelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useCallback } from "react";
import { trackEvent, getBaseTelemetryProperties, roundToBase2 } from "@/common/telemetry";
import { trackEvent, roundToBase2 } from "@/common/telemetry";
import type { ErrorContext } from "@/common/telemetry/payload";

/**
* Hook for clean telemetry integration in React components
*
* Provides type-safe telemetry tracking with base properties automatically included.
* Provides type-safe telemetry tracking. Base properties (version, platform, etc.)
* are automatically added by the backend TelemetryService.
*
* Usage:
*
* ```tsx
Expand All @@ -30,7 +32,6 @@ export function useTelemetry() {
trackEvent({
event: "workspace_switched",
properties: {
...getBaseTelemetryProperties(),
fromWorkspaceId,
toWorkspaceId,
},
Expand All @@ -42,7 +43,6 @@ export function useTelemetry() {
trackEvent({
event: "workspace_created",
properties: {
...getBaseTelemetryProperties(),
workspaceId,
},
});
Expand All @@ -53,7 +53,6 @@ export function useTelemetry() {
trackEvent({
event: "message_sent",
properties: {
...getBaseTelemetryProperties(),
model,
mode,
message_length_b2: roundToBase2(messageLength),
Expand All @@ -66,7 +65,6 @@ export function useTelemetry() {
trackEvent({
event: "error_occurred",
properties: {
...getBaseTelemetryProperties(),
errorType,
context,
},
Expand Down
12 changes: 0 additions & 12 deletions src/browser/utils/chatCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { DEFAULT_COMPACTION_WORD_TARGET, WORDS_TO_TOKENS_RATIO } from "@/common/
// ============================================================================

import { createCommandToast } from "@/browser/components/ChatInputToasts";
import { setTelemetryEnabled } from "@/common/telemetry";

export interface ForkOptions {
client: RouterClient<AppRouter>;
Expand Down Expand Up @@ -227,17 +226,6 @@ export async function processSlashCommand(
return { clearInput: true, toastShown: false };
}

if (parsed.type === "telemetry-set") {
setInput("");
setTelemetryEnabled(parsed.enabled);
setToast({
id: Date.now().toString(),
type: "success",
message: `Telemetry ${parsed.enabled ? "enabled" : "disabled"}`,
});
return { clearInput: true, toastShown: true };
}

// 2. Workspace Commands
const workspaceCommands = ["clear", "truncate", "compact", "fork", "new"];
const isWorkspaceCommand = workspaceCommands.includes(parsed.type);
Expand Down
42 changes: 1 addition & 41 deletions src/browser/utils/slashCommands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,46 +456,6 @@ const vimCommandDefinition: SlashCommandDefinition = {
},
};

const telemetryCommandDefinition: SlashCommandDefinition = {
key: "telemetry",
description: "Enable or disable telemetry",
handler: ({ cleanRemainingTokens }): ParsedCommand => {
if (cleanRemainingTokens.length === 0) {
return { type: "telemetry-help" };
}

if (cleanRemainingTokens.length === 1) {
const arg = cleanRemainingTokens[0].toLowerCase();
if (arg === "on" || arg === "off") {
return { type: "telemetry-set", enabled: arg === "on" };
}
}

return {
type: "unknown-command",
command: "telemetry",
subcommand: cleanRemainingTokens[0],
};
},
suggestions: ({ stage, partialToken }) => {
if (stage === 1) {
const options = [
{ key: "on", description: "Enable telemetry" },
{ key: "off", description: "Disable telemetry" },
];

return filterAndMapSuggestions(options, partialToken, (definition) => ({
id: `command:telemetry:${definition.key}`,
display: definition.key,
description: definition.description,
replacement: `/telemetry ${definition.key}`,
}));
}

return null;
},
};

const forkCommandDefinition: SlashCommandDefinition = {
key: "fork",
description:
Expand Down Expand Up @@ -631,7 +591,7 @@ export const SLASH_COMMAND_DEFINITIONS: readonly SlashCommandDefinition[] = [
compactCommandDefinition,
modelCommandDefinition,
providersCommandDefinition,
telemetryCommandDefinition,

forkCommandDefinition,
newCommandDefinition,
vimCommandDefinition,
Expand Down
2 changes: 0 additions & 2 deletions src/browser/utils/slashCommands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export type ParsedCommand =
| { type: "clear" }
| { type: "truncate"; percentage: number }
| { type: "compact"; maxOutputTokens?: number; continueMessage?: string; model?: string }
| { type: "telemetry-set"; enabled: boolean }
| { type: "telemetry-help" }
| { type: "fork"; newName: string; startMessage?: string }
| { type: "fork-help" }
| {
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async function createTestServer(authToken?: string): Promise<TestServerHandle> {
serverService: services.serverService,
menuEventService: services.menuEventService,
voiceService: services.voiceService,
telemetryService: services.telemetryService,
};

// Use the actual createOrpcServer function
Expand Down
1 change: 1 addition & 0 deletions src/cli/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ async function createTestServer(): Promise<TestServerHandle> {
serverService: services.serverService,
menuEventService: services.menuEventService,
voiceService: services.voiceService,
telemetryService: services.telemetryService,
};

// Use the actual createOrpcServer function
Expand Down
1 change: 1 addition & 0 deletions src/cli/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const mockWindow: BrowserWindow = {
serverService: serviceContainer.serverService,
menuEventService: serviceContainer.menuEventService,
voiceService: serviceContainer.voiceService,
telemetryService: serviceContainer.telemetryService,
};

const server = await createOrpcServer({
Expand Down
2 changes: 2 additions & 0 deletions src/common/orpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export {
providers,
ProvidersConfigMapSchema,
server,
telemetry,
TelemetryEventSchema,
terminal,
tokenizer,
update,
Expand Down
Loading