Skip to content

Commit 544bfe1

Browse files
committed
🤖 feat: move PostHog telemetry to backend to avoid adblock
This moves telemetry event capture from the browser (posthog-js) to the Electron main process (posthog-node). Events from the renderer are now forwarded via ORPC to the backend TelemetryService. Benefits: - Ad blockers cannot block telemetry requests from the main process - Centralized distinct ID management (persisted in ~/.mux/telemetry_id) - Base properties (version, platform, electronVersion) added by backend Changes: - Add posthog-node dependency - Create TelemetryService in src/node/services/ - Add ORPC telemetry.{track, setEnabled, isEnabled} routes - Update frontend client to call backend via ORPC - Simplify payload types (backend adds base properties) The frontend API (useTelemetry hook, trackEvent function) remains unchanged. _Generated with mux_
1 parent 5349cc6 commit 544bfe1

File tree

21 files changed

+394
-141
lines changed

21 files changed

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

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/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/cli/cli.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ async function createTestServer(authToken?: string): Promise<TestServerHandle> {
6767
serverService: services.serverService,
6868
menuEventService: services.menuEventService,
6969
voiceService: services.voiceService,
70+
telemetryService: services.telemetryService,
7071
};
7172

7273
// Use the actual createOrpcServer function

src/cli/server.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ async function createTestServer(): Promise<TestServerHandle> {
7070
serverService: services.serverService,
7171
menuEventService: services.menuEventService,
7272
voiceService: services.voiceService,
73+
telemetryService: services.telemetryService,
7374
};
7475

7576
// Use the actual createOrpcServer function

src/cli/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const mockWindow: BrowserWindow = {
7878
serverService: serviceContainer.serverService,
7979
menuEventService: serviceContainer.menuEventService,
8080
voiceService: serviceContainer.voiceService,
81+
telemetryService: serviceContainer.telemetryService,
8182
};
8283

8384
const server = await createOrpcServer({

src/common/orpc/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export {
9898
providers,
9999
ProvidersConfigMapSchema,
100100
server,
101+
telemetry,
102+
TelemetryEventSchema,
101103
terminal,
102104
tokenizer,
103105
update,

src/common/orpc/schemas/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
import { BashToolResultSchema, FileTreeNodeSchema } from "./tools";
1717
import { FrontendWorkspaceMetadataSchema, WorkspaceActivitySnapshotSchema } from "./workspace";
1818

19+
// Re-export telemetry schemas
20+
export { telemetry, TelemetryEventSchema } from "./telemetry";
21+
1922
// --- API Router Schemas ---
2023

2124
// Tokenizer
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Telemetry ORPC schemas
3+
*
4+
* Defines input/output schemas for backend telemetry endpoints.
5+
*/
6+
7+
import { z } from "zod";
8+
9+
// Error context enum (matches payload.ts)
10+
const ErrorContextSchema = z.enum([
11+
"workspace-creation",
12+
"workspace-deletion",
13+
"workspace-switch",
14+
"message-send",
15+
"message-stream",
16+
"project-add",
17+
"project-remove",
18+
"git-operation",
19+
]);
20+
21+
// Individual event payload schemas
22+
const AppStartedPropertiesSchema = z.object({
23+
isFirstLaunch: z.boolean(),
24+
});
25+
26+
const WorkspaceCreatedPropertiesSchema = z.object({
27+
workspaceId: z.string(),
28+
});
29+
30+
const WorkspaceSwitchedPropertiesSchema = z.object({
31+
fromWorkspaceId: z.string(),
32+
toWorkspaceId: z.string(),
33+
});
34+
35+
const MessageSentPropertiesSchema = z.object({
36+
model: z.string(),
37+
mode: z.string(),
38+
message_length_b2: z.number(),
39+
});
40+
41+
const ErrorOccurredPropertiesSchema = z.object({
42+
errorType: z.string(),
43+
context: ErrorContextSchema,
44+
});
45+
46+
// Union of all telemetry events
47+
export const TelemetryEventSchema = z.discriminatedUnion("event", [
48+
z.object({
49+
event: z.literal("app_started"),
50+
properties: AppStartedPropertiesSchema,
51+
}),
52+
z.object({
53+
event: z.literal("workspace_created"),
54+
properties: WorkspaceCreatedPropertiesSchema,
55+
}),
56+
z.object({
57+
event: z.literal("workspace_switched"),
58+
properties: WorkspaceSwitchedPropertiesSchema,
59+
}),
60+
z.object({
61+
event: z.literal("message_sent"),
62+
properties: MessageSentPropertiesSchema,
63+
}),
64+
z.object({
65+
event: z.literal("error_occurred"),
66+
properties: ErrorOccurredPropertiesSchema,
67+
}),
68+
]);
69+
70+
// API schemas
71+
export const telemetry = {
72+
track: {
73+
input: TelemetryEventSchema,
74+
output: z.void(),
75+
},
76+
setEnabled: {
77+
input: z.object({ enabled: z.boolean() }),
78+
output: z.void(),
79+
},
80+
isEnabled: {
81+
input: z.void(),
82+
output: z.boolean(),
83+
},
84+
};

src/common/telemetry/client.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
// Mock posthog-js to avoid import issues in test environment
2-
jest.mock("posthog-js", () => ({
3-
__esModule: true,
4-
default: {
5-
init: jest.fn(),
6-
capture: jest.fn(),
7-
reset: jest.fn(),
8-
},
9-
}));
10-
111
// Ensure NODE_ENV is set to test for telemetry detection
122
// Must be set before importing the client module
133
process.env.NODE_ENV = "test";
@@ -20,20 +10,18 @@ describe("Telemetry", () => {
2010
process.env.NODE_ENV = "test";
2111
});
2212

23-
it("should not initialize PostHog", () => {
13+
it("should not initialize", () => {
2414
initTelemetry();
2515
expect(isTelemetryInitialized()).toBe(false);
2616
});
2717

2818
it("should silently ignore track events", () => {
2919
// Should not throw even though not initialized
20+
// Base properties (version, platform, electronVersion) are now added by backend
3021
expect(() => {
3122
trackEvent({
3223
event: "workspace_switched",
3324
properties: {
34-
version: "1.0.0",
35-
platform: "darwin",
36-
electronVersion: "28.0.0",
3725
fromWorkspaceId: "test-from",
3826
toWorkspaceId: "test-to",
3927
},

0 commit comments

Comments
 (0)