Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 1dda85f

Browse files
committed
feat: add analytics post test
1 parent 607672d commit 1dda85f

13 files changed

Lines changed: 465 additions & 22 deletions

File tree

apps/array/src/main/services/posthog-analytics.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PostHog } from "posthog-node";
22

33
let posthogClient: PostHog | null = null;
4+
let currentUserId: string | null = null;
45

56
export function initializePostHog() {
67
if (posthogClient) {
@@ -21,6 +22,14 @@ export function initializePostHog() {
2122
return posthogClient;
2223
}
2324

25+
export function setCurrentUserId(userId: string | null) {
26+
currentUserId = userId;
27+
}
28+
29+
export function getCurrentUserId() {
30+
return currentUserId;
31+
}
32+
2433
export function trackAppEvent(
2534
eventName: string,
2635
properties?: Record<string, string | number | boolean>,
@@ -29,13 +38,16 @@ export function trackAppEvent(
2938
return;
3039
}
3140

41+
// Use real user ID if available, otherwise use anonymous ID
42+
const distinctId = currentUserId || "anonymous-app-event";
43+
3244
properties = {
3345
...properties,
34-
$process_person_profile: false,
46+
$process_person_profile: !!currentUserId,
3547
};
3648

3749
posthogClient.capture({
38-
distinctId: "app-event",
50+
distinctId,
3951
event: eventName,
4052
properties,
4153
});
@@ -49,6 +61,8 @@ export function identifyUser(
4961
return;
5062
}
5163

64+
currentUserId = userId;
65+
5266
posthogClient.identify({
5367
distinctId: userId,
5468
properties,
@@ -65,3 +79,7 @@ export async function shutdownPostHog() {
6579
export function getPostHogClient() {
6680
return posthogClient;
6781
}
82+
83+
export function resetUser() {
84+
currentUserId = null;
85+
}

apps/array/src/main/trpc/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { agentRouter } from "./routers/agent.js";
2+
import { analyticsRouter } from "./routers/analytics.js";
23
import { contextMenuRouter } from "./routers/context-menu.js";
34
import { deepLinkRouter } from "./routers/deep-link.js";
45
import { dockBadgeRouter } from "./routers/dock-badge.js";
@@ -20,6 +21,7 @@ import { router } from "./trpc.js";
2021

2122
export const trpcRouter = router({
2223
agent: agentRouter,
24+
analytics: analyticsRouter,
2325
contextMenu: contextMenuRouter,
2426
dockBadge: dockBadgeRouter,
2527
encryption: encryptionRouter,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { z } from "zod";
2+
import {
3+
identifyUser,
4+
resetUser,
5+
setCurrentUserId,
6+
} from "../../services/posthog-analytics.js";
7+
import { publicProcedure, router } from "../trpc.js";
8+
9+
export const analyticsRouter = router({
10+
/**
11+
* Set the current user ID for main process analytics
12+
*/
13+
setUserId: publicProcedure
14+
.input(
15+
z.object({
16+
userId: z.string(),
17+
properties: z
18+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
19+
.optional(),
20+
}),
21+
)
22+
.mutation(({ input }) => {
23+
setCurrentUserId(input.userId);
24+
if (input.properties) {
25+
identifyUser(
26+
input.userId,
27+
input.properties as Record<string, string | number | boolean>,
28+
);
29+
}
30+
}),
31+
32+
/**
33+
* Reset the current user (on logout)
34+
*/
35+
resetUser: publicProcedure.mutation(() => {
36+
resetUser();
37+
}),
38+
});

apps/array/src/renderer/features/auth/stores/authStore.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ export const useAuthStore = create<AuthState>()(
133133
project_id: projectId.toString(),
134134
region,
135135
});
136+
137+
trpcVanilla.analytics.setUserId.mutate({
138+
userId: user.uuid,
139+
properties: {
140+
project_id: projectId.toString(),
141+
region,
142+
},
143+
});
136144
} catch {
137145
throw new Error("Failed to authenticate with PostHog");
138146
}
@@ -306,6 +314,14 @@ export const useAuthStore = create<AuthState>()(
306314
region: tokens.cloudRegion,
307315
});
308316

317+
trpcVanilla.analytics.setUserId.mutate({
318+
userId: user.uuid,
319+
properties: {
320+
project_id: projectId.toString(),
321+
region: tokens.cloudRegion,
322+
},
323+
});
324+
309325
return true;
310326
} catch (error) {
311327
log.error("Failed to validate OAuth session:", error);
@@ -321,6 +337,8 @@ export const useAuthStore = create<AuthState>()(
321337
track(ANALYTICS_EVENTS.USER_LOGGED_OUT);
322338
resetUser();
323339

340+
trpcVanilla.analytics.resetUser.mutate();
341+
324342
if (refreshTimeoutId) {
325343
window.clearTimeout(refreshTimeoutId);
326344
refreshTimeoutId = null;

apps/array/src/renderer/features/command/components/CommandMenu.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
ViewVerticalIcon,
1212
} from "@radix-ui/react-icons";
1313
import { Flex, Text } from "@radix-ui/themes";
14+
import { track } from "@renderer/lib/analytics";
1415
import { useNavigationStore } from "@stores/navigationStore";
1516
import { useRegisteredFoldersStore } from "@stores/registeredFoldersStore";
1617
import { useThemeStore } from "@stores/themeStore";
1718
import { useCallback, useEffect, useRef } from "react";
1819
import { useHotkeys } from "react-hotkeys-hook";
20+
import { ANALYTICS_EVENTS, type CommandMenuAction } from "@/types/analytics";
1921

2022
interface CommandMenuProps {
2123
open: boolean;
@@ -32,15 +34,31 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
3234

3335
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
3436

37+
const trackAction = useCallback((action: CommandMenuAction) => {
38+
track(ANALYTICS_EVENTS.COMMAND_MENU_ACTION, { action_type: action });
39+
}, []);
40+
3541
const runAndClose = useCallback(
36-
<T extends unknown[]>(fn: (...args: T) => void) =>
42+
<T extends unknown[]>(
43+
fn: (...args: T) => void,
44+
action?: CommandMenuAction,
45+
) =>
3746
(...args: T) => {
47+
if (action) {
48+
trackAction(action);
49+
}
3850
fn(...args);
3951
close();
4052
},
41-
[close],
53+
[close, trackAction],
4254
);
4355

56+
useEffect(() => {
57+
if (open) {
58+
track(ANALYTICS_EVENTS.COMMAND_MENU_OPENED);
59+
}
60+
}, [open]);
61+
4462
useHotkeys("escape", close, {
4563
enabled: open,
4664
enableOnContentEditable: true,
@@ -107,14 +125,14 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
107125
<Command.Group heading="Navigation">
108126
<Command.Item
109127
value="Home"
110-
onSelect={runAndClose(navigateToTaskInput)}
128+
onSelect={runAndClose(navigateToTaskInput, "home")}
111129
>
112130
<HomeIcon className="mr-3 h-3 w-3 text-gray-11" />
113131
<Text size="1">Home</Text>
114132
</Command.Item>
115133
<Command.Item
116134
value="Settings"
117-
onSelect={runAndClose(navigateToSettings)}
135+
onSelect={runAndClose(navigateToSettings, "settings")}
118136
>
119137
<GearIcon className="mr-3 h-3 w-3 text-gray-11" />
120138
<Text size="1">Settings</Text>
@@ -124,7 +142,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
124142
<Command.Group heading="Actions">
125143
<Command.Item
126144
value="Toggle theme dark light mode"
127-
onSelect={runAndClose(toggleDarkMode)}
145+
onSelect={runAndClose(toggleDarkMode, "toggle-theme")}
128146
>
129147
{isDarkMode ? (
130148
<SunIcon className="mr-3 h-3 w-3 text-gray-11" />
@@ -135,21 +153,24 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
135153
</Command.Item>
136154
<Command.Item
137155
value="Toggle left sidebar"
138-
onSelect={runAndClose(toggleLeftSidebar)}
156+
onSelect={runAndClose(toggleLeftSidebar, "toggle-left-sidebar")}
139157
>
140158
<ViewVerticalIcon className="mr-3 h-3 w-3 text-gray-11" />
141159
<Text size="1">Toggle left sidebar</Text>
142160
</Command.Item>
143161
<Command.Item
144162
value="Toggle right sidebar"
145-
onSelect={runAndClose(toggleRightSidebar)}
163+
onSelect={runAndClose(
164+
toggleRightSidebar,
165+
"toggle-right-sidebar",
166+
)}
146167
>
147168
<ViewVerticalIcon className="mr-3 h-3 w-3 rotate-180 text-gray-11" />
148169
<Text size="1">Toggle right sidebar</Text>
149170
</Command.Item>
150171
<Command.Item
151172
value="Create new task"
152-
onSelect={runAndClose(navigateToTaskInput)}
173+
onSelect={runAndClose(navigateToTaskInput, "new-task")}
153174
>
154175
<FileTextIcon className="mr-3 h-3 w-3 text-gray-11" />
155176
<Text size="1">New task</Text>
@@ -162,7 +183,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
162183
<Command.Item
163184
key={folder.id}
164185
value={`New task in ${folder.name} folder ${folder.path}`}
165-
onSelect={runAndClose(() => navigateToTaskInput(folder.id))}
186+
onSelect={runAndClose(
187+
() => navigateToTaskInput(folder.id),
188+
"new-task",
189+
)}
166190
>
167191
<FileTextIcon className="mr-3 h-3 w-3 text-gray-11" />
168192
<Text size="1">

apps/array/src/renderer/features/panels/store/panelLayoutStore.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { track } from "@renderer/lib/analytics";
12
import { persist } from "zustand/middleware";
23
import { createWithEqualityFn } from "zustand/traditional";
4+
import { ANALYTICS_EVENTS } from "@/types/analytics";
35
import {
46
DEFAULT_PANEL_IDS,
57
DEFAULT_TAB_IDS,
@@ -28,6 +30,11 @@ import {
2830
} from "./panelTree";
2931
import type { PanelNode, Tab } from "./panelTypes";
3032

33+
function getFileExtension(filePath: string): string {
34+
const parts = filePath.split(".");
35+
return parts.length > 1 ? parts[parts.length - 1] : "";
36+
}
37+
3138
export interface TaskLayout {
3239
panelTree: PanelNode;
3340
openFiles: string[];
@@ -282,11 +289,30 @@ export const usePanelLayoutStore = createWithEqualityFn<PanelLayoutStore>()(
282289
openFile: (taskId, filePath, asPreview = true) => {
283290
const tabId = createFileTabId(filePath);
284291
set((state) => openTab(state, taskId, tabId, asPreview));
292+
293+
track(ANALYTICS_EVENTS.FILE_OPENED, {
294+
file_extension: getFileExtension(filePath),
295+
source: "sidebar",
296+
task_id: taskId,
297+
});
285298
},
286299

287300
openDiff: (taskId, filePath, status, asPreview = true) => {
288301
const tabId = createDiffTabId(filePath, status);
289302
set((state) => openTab(state, taskId, tabId, asPreview));
303+
304+
// Track diff viewed
305+
const changeType =
306+
status === "added"
307+
? "added"
308+
: status === "deleted"
309+
? "deleted"
310+
: "modified";
311+
track(ANALYTICS_EVENTS.FILE_DIFF_VIEWED, {
312+
file_extension: getFileExtension(filePath),
313+
change_type: changeType,
314+
task_id: taskId,
315+
});
290316
},
291317

292318
keepTab: (taskId, panelId, tabId) => {

0 commit comments

Comments
 (0)