-
Notifications
You must be signed in to change notification settings - Fork 928
feat: add PostHog event tracking across apps #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,27 @@ | ||
| "use client"; | ||
|
|
||
| import { useUser } from "@clerk/nextjs"; | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import posthog from "posthog-js"; | ||
| import { useEffect } from "react"; | ||
| import { useTRPC } from "../../trpc/react"; | ||
|
|
||
| export function PostHogUserIdentifier() { | ||
| const { user, isLoaded } = useUser(); | ||
| const { isSignedIn } = useUser(); | ||
| const trpc = useTRPC(); | ||
|
|
||
| useEffect(() => { | ||
| if (!isLoaded) return; | ||
| const { data: user } = useQuery({ | ||
| ...trpc.user.me.queryOptions(), | ||
| enabled: isSignedIn, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (user) { | ||
| posthog.identify(user.id, { | ||
| email: user.primaryEmailAddress?.emailAddress, | ||
| name: user.fullName, | ||
| }); | ||
| } else { | ||
| posthog.identify(user.id, { email: user.email, name: user.name }); | ||
| } else if (isSignedIn === false) { | ||
| posthog.reset(); | ||
| } | ||
| }, [user, isLoaded]); | ||
| }, [user, isSignedIn]); | ||
|
|
||
| return null; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { track } from "main/lib/analytics"; | ||
| import { db } from "main/lib/db"; | ||
| import { terminalManager } from "main/lib/terminal"; | ||
| import { nanoid } from "nanoid"; | ||
|
|
@@ -166,6 +167,13 @@ export const createWorkspacesRouter = () => { | |
| // Load setup configuration from the main repo (where .superset/config.json lives) | ||
| const setupConfig = loadSetupConfig(project.mainRepoPath); | ||
|
|
||
| track("workspace_created", { | ||
| workspace_id: workspace.id, | ||
| project_id: project.id, | ||
| branch: branch, | ||
| base_branch: targetBranch, | ||
| }); | ||
|
|
||
|
Comment on lines
167
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionConsider tracking only after the full mutation has succeeded (i.e., immediately before returning, after all possible failure points), or wrap the mutation body in a Example pattern:
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor across the workspace mutations. |
||
| return { | ||
| workspace, | ||
| initialCommands: setupConfig?.setup || null, | ||
|
|
@@ -271,6 +279,13 @@ export const createWorkspacesRouter = () => { | |
| } | ||
| }); | ||
|
|
||
| track("workspace_opened", { | ||
| workspace_id: returnedWorkspace.id, | ||
| project_id: project.id, | ||
| type: "branch", | ||
| was_existing: wasExisting, | ||
| }); | ||
|
|
||
| return { | ||
| workspace: returnedWorkspace, | ||
| worktreePath: project.mainRepoPath, | ||
|
|
@@ -772,6 +787,8 @@ export const createWorkspacesRouter = () => { | |
| ? `${terminalResult.failed} terminal process(es) may still be running` | ||
| : undefined; | ||
|
|
||
| track("workspace_deleted", { workspace_id: input.id }); | ||
|
|
||
| return { success: true, teardownError, terminalWarning }; | ||
| }), | ||
|
|
||
|
|
@@ -1059,6 +1076,12 @@ export const createWorkspacesRouter = () => { | |
| // Load setup configuration from the main repo | ||
| const setupConfig = loadSetupConfig(project.mainRepoPath); | ||
|
|
||
| track("workspace_opened", { | ||
| workspace_id: workspace.id, | ||
| project_id: project.id, | ||
| type: "worktree", | ||
| }); | ||
|
|
||
| return { | ||
| workspace, | ||
| initialCommands: setupConfig?.setup || null, | ||
|
|
@@ -1110,6 +1133,8 @@ export const createWorkspacesRouter = () => { | |
| ? `${terminalResult.failed} terminal process(es) may still be running` | ||
| : undefined; | ||
|
|
||
| track("workspace_closed", { workspace_id: input.id }); | ||
|
|
||
| return { success: true, terminalWarning }; | ||
| }), | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { env } from "main/env.main"; | ||
| import { apiClient } from "main/lib/api-client"; | ||
| import { PostHog } from "posthog-node"; | ||
|
|
||
| let client: PostHog | null = null; | ||
| let cachedUserId: string | null = null; | ||
|
|
||
| function getClient(): PostHog | null { | ||
| if (!env.NEXT_PUBLIC_POSTHOG_KEY) { | ||
| return null; | ||
| } | ||
|
|
||
| if (!client) { | ||
| client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, { | ||
| host: env.NEXT_PUBLIC_POSTHOG_HOST, | ||
| flushAt: 1, // Send events immediately for desktop app | ||
| flushInterval: 0, | ||
| }); | ||
| } | ||
|
Comment on lines
+13
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionUse a small batch/interval to reduce overhead (e.g., Reply with "@CharlieHelps yes please" if you'd like me to add a commit adjusting the PostHog client config and adding an optional |
||
| return client; | ||
| } | ||
|
|
||
| async function getUserId(): Promise<string | null> { | ||
| if (cachedUserId) return cachedUserId; | ||
| try { | ||
| const user = await apiClient.user.me.query(); | ||
| cachedUserId = user?.id ?? null; | ||
| return cachedUserId; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clear cached user ID (call on sign out) | ||
| */ | ||
| export function clearUserCache(): void { | ||
| cachedUserId = null; | ||
| } | ||
|
|
||
| /** | ||
| * Track an event with the current user's ID as distinct_id. | ||
| * Fire-and-forget - errors are silently ignored. | ||
| */ | ||
| export function track( | ||
| event: string, | ||
| properties?: Record<string, unknown>, | ||
| ): void { | ||
| const posthog = getClient(); | ||
| if (!posthog) return; | ||
|
|
||
| getUserId() | ||
| .then((userId) => { | ||
| if (!userId) return; | ||
| posthog.capture({ | ||
| distinctId: userId, | ||
| event, | ||
| properties: { | ||
| ...properties, | ||
| app_name: "desktop", | ||
| platform: process.platform, | ||
| }, | ||
| }); | ||
| }) | ||
| .catch(() => {}); | ||
| } | ||
|
|
||
| /** | ||
| * Shutdown PostHog client (call on app quit) | ||
| */ | ||
| export async function shutdown(): Promise<void> { | ||
| await client?.shutdown(); | ||
| } | ||
|
Comment on lines
+1
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The analytics module drops events whenever Also, swallowing all errors ( SuggestionImprove reliability/observability while keeping it low-noise:
Example: let warned = false;
...
.catch((err) => {
if (!warned && process.env.NODE_ENV !== 'test') {
warned = true;
console.warn('[analytics] capture failed', err);
}
});Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing (1) and a minimal (3). |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { EventEmitter } from "node:events"; | ||
| import { track } from "main/lib/analytics"; | ||
| import { FALLBACK_SHELL, SHELL_CRASH_THRESHOLD_MS } from "./env"; | ||
| import { | ||
| closeSessionHistory, | ||
|
|
@@ -58,7 +59,7 @@ export class TerminalManager extends EventEmitter { | |
| private async doCreateSession( | ||
| params: InternalCreateSessionParams, | ||
| ): Promise<SessionResult> { | ||
| const { paneId, initialCommands } = params; | ||
| const { paneId, workspaceId, initialCommands } = params; | ||
|
|
||
| // Create the session | ||
| const session = await createSession(params, (id, data) => { | ||
|
|
@@ -75,6 +76,9 @@ export class TerminalManager extends EventEmitter { | |
|
|
||
| this.sessions.set(paneId, session); | ||
|
|
||
| // Track terminal opened (only fires once per session creation) | ||
| track("terminal_opened", { workspace_id: workspaceId, pane_id: paneId }); | ||
|
|
||
|
Comment on lines
+79
to
+81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
SuggestionGate the event to truly-new sessions only (e.g., Reply with "@CharlieHelps yes please" if you'd like me to add a commit that includes |
||
| return { | ||
| isNew: true, | ||
| scrollback: session.scrollback, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,24 @@ | ||
| import { COMPANY } from "@superset/shared/constants"; | ||
| import { Button } from "@superset/ui/button"; | ||
| import { useEffect } from "react"; | ||
| import { FaGithub } from "react-icons/fa"; | ||
| import { FcGoogle } from "react-icons/fc"; | ||
| import { posthog } from "renderer/lib/posthog"; | ||
| import { trpc } from "renderer/lib/trpc"; | ||
| import type { AuthProvider } from "shared/auth"; | ||
| import { SupersetLogo } from "./components/SupersetLogo"; | ||
|
|
||
| export function SignInScreen() { | ||
| const signInMutation = trpc.auth.signIn.useMutation(); | ||
|
|
||
| const signIn = (provider: AuthProvider) => | ||
| useEffect(() => { | ||
| posthog.capture("desktop_opened"); | ||
| }, []); | ||
|
|
||
| const signIn = (provider: AuthProvider) => { | ||
| posthog.capture("auth_started", { provider }); | ||
| signInMutation.mutate({ provider }); | ||
| }; | ||
|
|
||
|
Comment on lines
11
to
22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Since you already introduced a main-process analytics module, app-open should be tracked from the main process (e.g., when SuggestionMove Example main process: import { track } from './lib/analytics';
...
await app.whenReady();
track('desktop_opened');Reply with "@CharlieHelps yes please" if you’d like me to add a commit moving the event and adjusting naming. |
||
| return ( | ||
| <div className="flex flex-col h-full w-full bg-background"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
posthog.reset()is gated onisSignedIn === false, but not on themequery being finished. If the query is still in-flight anduserisundefined, this effect currently does nothing (good), but whenisSignedInflips tofalseyou reset immediately even if there’s still cached React Query data foruser.me(or a staleuservalue). Consider resetting based on query state (e.g.,isFetched/isSuccess) to avoid edge cases where you briefly identify with a stale user after sign-out or during session transitions.Suggestion
Consider explicitly using the React Query status to drive reset/identify:
isSuccess/isFetched(orstatus) fromuseQuery.posthog.identifywhenisSuccess && user.posthog.resetwhenisSuccess && !useror whenisSignedIn === falseand the query has settled.Example:
Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing this change.