Skip to content

feat: add PostHog event tracking across apps#476

Merged
saddlepaddle merged 1 commit intomainfrom
witty-coyote-54ab27
Dec 23, 2025
Merged

feat: add PostHog event tracking across apps#476
saddlepaddle merged 1 commit intomainfrom
witty-coyote-54ab27

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Dec 22, 2025

Summary

  • Add custom PostHog event tracking for key user actions across marketing, web, admin, and desktop apps
  • Fix user identity to use database user ID consistently (was using Clerk ID in web/admin which differed from desktop)
  • Add posthog-node for reliable server-side tracking in desktop main process

Events Added

App Event Trigger
Marketing download_clicked Download button clicked
Marketing waitlist_clicked Waitlist button clicked
Desktop desktop_opened App launched
Desktop auth_started Sign-in button clicked
Desktop auth_completed OAuth flow completed
Desktop workspace_created New workspace created
Desktop workspace_opened Workspace opened
Desktop workspace_closed Workspace closed
Desktop workspace_deleted Workspace deleted
Desktop terminal_opened Terminal session created

Test plan

  • Lint passes
  • Typecheck passes
  • Tests pass (385/385)
  • Manually verified events appear in PostHog Live Events

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added comprehensive event tracking for authentication flows, workspace management, terminal sessions, and user downloads
    • Improved user identification to provide better analytics insights into product usage
  • Chores

    • Added analytics infrastructure and third-party dependencies
    • Updated environment configuration to support analytics across applications

✏️ Tip: You can customize this high-level summary in your review settings.

Add custom event tracking for key user actions:
- Marketing: download_clicked, waitlist_clicked
- Desktop: desktop_opened, auth_started, auth_completed,
  workspace_created/opened/closed/deleted, terminal_opened
- Fix user identity to use database user ID consistently across all apps
  (was using Clerk ID in web/admin which differed from desktop)

Also adds posthog-node for reliable server-side tracking in desktop main process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 22, 2025

Walkthrough

Integrates PostHog analytics across the application by adding a desktop PostHog client, replacing Clerk-based user identification with TRPC queries in PostHogUserIdentifier components, introducing analytics tracking for workspace and terminal lifecycle events, and configuring required environment variables.

Changes

Cohort / File(s) Summary
PostHog Client & Analytics Infrastructure
apps/desktop/src/main/lib/analytics/index.ts
apps/desktop/package.json
Adds posthog-node dependency and creates a new analytics module that initializes a PostHog client with lazy loading, user ID caching, and fire-and-forget event tracking. Exports track(), clearUserCache(), and shutdown() functions.
PostHog User Identification Components
apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
Replaces Clerk's useUser loading state with TRPC-driven trpc.user.me queries. Updates identification logic to use TRPC-fetched user data (email, name) and resets PostHog when signed out instead of when loading completes.
Analytics Tracking in TRPC Routers
apps/desktop/src/lib/trpc/routers/auth/index.ts
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Adds analytics tracking to auth sign-out (clears user cache) and workspace lifecycle events (created, opened, deleted, closed) with relevant metadata.
Terminal Analytics & Test Updates
apps/desktop/src/main/lib/terminal/manager.ts
apps/desktop/src/main/lib/terminal/manager.test.ts
Tracks "terminal_opened" events with workspace and pane IDs. Reorders test setup variable initialization after mock setup without changing logic.
Environment Configuration
apps/desktop/src/main/env.main.ts
Adds NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST environment variables with defaults and propagates them to runtimeEnv.
Main App & Renderer Integration
apps/desktop/src/main/index.ts
apps/desktop/src/renderer/screens/sign-in/index.tsx
Tracks "auth_completed" and "auth_started" events in authentication flow. Runs analytics shutdown in parallel with terminal cleanup on app quit.
UI Components with Analytics
apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
Captures "download_clicked" and "waitlist_clicked" events when users interact with download and waitlist buttons.
Test Mocks
apps/desktop/test-setup.ts
Adds electron shell.openExternal mock and analytics module mocks exposing track(), clearUserCache(), and shutdown() functions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • superset-sh/superset#419: Directly modifies the same PostHog initialization codepaths, environment variables, and PostHogUserIdentifier components across web/desktop/admin apps.
  • superset-sh/superset#340: Updates PostHogUserIdentifier components and auth/TRPC user plumbing (trpc.user.me endpoint and Clerk session usage) that this PR depends on.
  • superset-sh/superset#421: Modifies desktop PostHog environment configuration (apps/desktop/src/main/env.main.ts) and related runtime build setup.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description is comprehensive, providing a clear summary, detailed table of events added, and test results. However, it does not follow the repository's template structure with required sections like 'Related Issues', 'Type of Change', 'Testing', and 'Screenshots'. Consider restructuring the description to follow the template format with explicit sections for Related Issues, Type of Change checkbox, and Testing methodology for consistency with repository standards.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: add PostHog event tracking across apps' directly and clearly summarizes the main change: introducing PostHog event tracking functionality across multiple applications.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch witty-coyote-54ab27

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)

1-27: Consider extracting shared logic.

This component is nearly identical to apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx. If maintaining these in sync becomes burdensome, consider extracting the shared logic into a utility or shared package.

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (1)

1079-1083: Consider adding was_existing property for consistency.

The workspace_opened event for worktree workspaces is missing the was_existing property that's present in the branch workspace version (line 286). For worktrees, this would always be false since openWorktree throws if the workspace already exists (line 1014), but including it explicitly maintains consistency across event schemas.

Proposed change for consistency
 track("workspace_opened", {
   workspace_id: workspace.id,
   project_id: project.id,
   type: "worktree",
+  was_existing: false,
 });
apps/desktop/src/main/lib/analytics/index.ts (1)

8-21: Consider adding initialization guard for concurrent calls.

The getClient() function could create multiple PostHog instances if called concurrently before the first initialization completes. While unlikely in practice (desktop app initialization is mostly sequential), adding a guard would make the code more robust.

Proposed fix to prevent race conditions
 let client: PostHog | null = null;
+let clientInitializing = false;
 let cachedUserId: string | null = null;
 
 function getClient(): PostHog | null {
   if (!env.NEXT_PUBLIC_POSTHOG_KEY) {
     return null;
   }
 
-  if (!client) {
+  if (!client && !clientInitializing) {
+    clientInitializing = true;
     client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
       host: env.NEXT_PUBLIC_POSTHOG_HOST,
       flushAt: 1, // Send events immediately for desktop app
       flushInterval: 0,
     });
+    clientInitializing = false;
   }
   return client;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bffcc6 and b908dad.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/package.json
  • apps/desktop/src/lib/trpc/routers/auth/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/desktop/src/main/env.main.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
  • apps/desktop/test-setup.ts
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid using any type in TypeScript - maintain type safety unless absolutely necessary

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/lib/trpc/routers/auth/index.ts
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/env.main.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run Biome for formatting, linting, import organization, and safe fixes at the root level using bun run lint:fix

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/lib/trpc/routers/auth/index.ts
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/env.main.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
**/{components,features}/**/[!.]*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Organize project structure with one folder per component: ComponentName/ComponentName.tsx with index.ts barrel export

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
**/{components,features}/**/*.{ts,tsx,test.ts,test.tsx,stories.tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Co-locate component dependencies (utils, hooks, constants, config, tests, stories) next to the file using them

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
apps/desktop/src/renderer/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never import Node.js modules in renderer process or shared code - use only in main process (src/main/)

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
apps/desktop/src/{main,renderer,preload}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use type-safe IPC communication - define channel types in apps/desktop/src/shared/ipc-channels.ts before implementing handlers

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/main/env.main.ts
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use React + TailwindCSS v4 + shadcn/ui for UI development

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
**/{components,features}/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/{components,features}/**/*.tsx: Nest components in parent's components/ folder if used only once, promote to highest shared parent's components/ if used 2+ times
Use one component per file - do not combine multiple components in a single file

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx
  • apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/desktop/AGENTS.md)

apps/desktop/**/*.{ts,tsx}: For Electron interprocess communication, ALWAYS use tRPC as defined in src/lib/trpc
Use alias as defined in tsconfig.json when possible
Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.
For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from @trpc/server/observable instead of async generators, as the library explicitly checks isObservable(result) and throws an error otherwise

Files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/lib/trpc/routers/auth/index.ts
  • apps/desktop/src/main/env.main.ts
  • apps/desktop/test-setup.ts
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
  • apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
apps/desktop/src/main/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Accept object parameters in IPC handlers - do not use positional parameters in ipcMain.handle()

Files:

  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/lib/terminal/manager.ts
  • apps/desktop/src/main/lib/terminal/manager.test.ts
  • apps/desktop/src/main/lib/analytics/index.ts
  • apps/desktop/src/main/env.main.ts
apps/desktop/src/main/index.ts

📄 CodeRabbit inference engine (AGENTS.md)

Load environment variables in src/main/index.ts and electron.vite.config.ts with override: true before other imports

Files:

  • apps/desktop/src/main/index.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Place tests co-located with components using .test.ts or .test.tsx naming convention

Files:

  • apps/desktop/src/main/lib/terminal/manager.test.ts
🧠 Learnings (7)
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary.

Applied to files:

  • apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx
  • apps/desktop/src/renderer/screens/sign-in/index.tsx
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/main/**/*.ts : Accept object parameters in IPC handlers - do not use positional parameters in ipcMain.handle()

Applied to files:

  • apps/desktop/src/main/lib/terminal/manager.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/*.test.{ts,tsx} : Place tests co-located with components using .test.ts or .test.tsx naming convention

Applied to files:

  • apps/desktop/src/main/lib/terminal/manager.test.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to **/*.{tsx,css} : Use React + TailwindCSS v4 + shadcn/ui for UI development

Applied to files:

  • apps/desktop/package.json
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For tRPC subscriptions with trpc-electron, ALWAYS use the observable pattern from `trpc/server/observable` instead of async generators, as the library explicitly checks `isObservable(result)` and throws an error otherwise

Applied to files:

  • apps/desktop/src/lib/trpc/routers/auth/index.ts
📚 Learning: 2025-12-21T04:39:28.543Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: apps/desktop/AGENTS.md:0-0
Timestamp: 2025-12-21T04:39:28.543Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : For Electron interprocess communication, ALWAYS use tRPC as defined in `src/lib/trpc`

Applied to files:

  • apps/desktop/src/lib/trpc/routers/auth/index.ts
📚 Learning: 2025-12-18T23:19:10.415Z
Learnt from: CR
Repo: superset-sh/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T23:19:10.415Z
Learning: Applies to apps/desktop/src/main/index.ts : Load environment variables in src/main/index.ts and electron.vite.config.ts with override: true before other imports

Applied to files:

  • apps/desktop/src/main/env.main.ts
🧬 Code graph analysis (9)
apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)
apps/desktop/src/renderer/lib/posthog.ts (1)
  • posthog (28-28)
apps/desktop/src/main/index.ts (2)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • track (45-66)
apps/desktop/src/main/lib/terminal/manager.ts (1)
  • terminalManager (432-432)
apps/desktop/src/main/lib/terminal/manager.ts (1)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • track (45-66)
apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx (1)
apps/desktop/src/renderer/lib/posthog.ts (1)
  • posthog (28-28)
apps/desktop/src/main/lib/analytics/index.ts (2)
apps/desktop/src/main/env.main.ts (1)
  • env (12-41)
apps/desktop/src/main/lib/api-client.ts (1)
  • apiClient (11-27)
apps/desktop/src/lib/trpc/routers/auth/index.ts (1)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • clearUserCache (37-39)
apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)
apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)
  • PostHogUserIdentifier (6-18)
apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (1)
apps/desktop/src/main/lib/analytics/index.ts (1)
  • track (45-66)
apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (2)
apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)
  • PostHogUserIdentifier (9-27)
apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)
  • PostHogUserIdentifier (6-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Deploy Web
  • GitHub Check: Deploy API
  • GitHub Check: Deploy Admin
  • GitHub Check: Deploy Docs
  • GitHub Check: Deploy Marketing
  • GitHub Check: Lint
  • GitHub Check: Build
🔇 Additional comments (21)
apps/desktop/src/renderer/screens/sign-in/index.tsx (2)

18-21: LGTM! Auth event tracking is well-placed.

Capturing "auth_started" with the provider before triggering the mutation is appropriate. The event will fire exactly once per sign-in attempt and provides useful context with the provider parameter.


14-16: and

apps/desktop/package.json (1)

82-82: LGTM!

Adding posthog-node for server-side analytics in the Electron main process is the correct approach. This enables reliable event tracking from the main process where posthog-js (browser-based) cannot be used directly.

apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx (2)

24-27: LGTM!

The tracking is placed before the action (opening URL), ensuring the event is captured even if the user navigates away. This is a reliable pattern for analytics.


81-84: LGTM!

Good pattern: capturing the event before invoking the callback ensures analytics are recorded regardless of callback behavior.

apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)

6-17: LGTM!

The implementation correctly uses isSuccess to differentiate between "query still loading" and "query completed with no user data". This ensures posthog.reset() is only called after confirming the user is actually unauthenticated, not during initial load.

apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)

9-27: LGTM!

The implementation correctly:

  1. Conditionally enables the query only when isSignedIn is true (avoiding unnecessary requests)
  2. Uses strict equality (isSignedIn === false) to prevent reset during initial undefined state
  3. Sources identity from the TRPC user query (database ID) instead of Clerk, ensuring consistency across platforms as per the PR objectives

The pattern matches the admin implementation, maintaining consistency across apps.

apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx (1)

9-27: LGTM!

The implementation is consistent with the web app's PostHogUserIdentifier, correctly sourcing user identity from TRPC to use the database user ID.

apps/desktop/src/main/env.main.ts (1)

21-22: LGTM! Environment configuration for PostHog is correct.

The optional NEXT_PUBLIC_POSTHOG_KEY allows analytics to be disabled when not configured, and the default host value ensures the app works out-of-the-box when a key is provided.

Also applies to: 34-35

apps/desktop/src/main/lib/terminal/manager.test.ts (1)

14-15: LGTM! Test setup reordering.

The test directory initialization has been moved to improve organization. No functional changes to the test logic.

apps/desktop/src/lib/trpc/routers/auth/index.ts (1)

3-3: LGTM! User cache cleared on sign out.

The clearUserCache() call correctly clears the cached user identity after sign out, ensuring subsequent events won't be attributed to the signed-out user.

Also applies to: 57-57

apps/desktop/src/main/lib/terminal/manager.ts (1)

2-2: LGTM! Terminal opened event tracked correctly.

The terminal_opened event is emitted after successful session creation with relevant context (workspace_id, pane_id). The fire-and-forget pattern is appropriate for telemetry.

Also applies to: 62-62, 79-80

apps/desktop/src/main/index.ts (2)

6-6: LGTM! Auth completion tracked.

The auth_completed event is tracked after successful authentication, providing visibility into the auth flow.

Also applies to: 52-52


143-145: LGTM! Parallel cleanup improves shutdown performance.

Running terminal cleanup and analytics shutdown in parallel with Promise.all reduces app quit time without introducing risk since the operations are independent.

apps/desktop/test-setup.ts (1)

88-90: LGTM! Test mocks complete.

The electron shell and analytics mocks provide the necessary test infrastructure for components that use these APIs, preventing tests from making real external calls.

Also applies to: 93-101

apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts (3)

170-175: LGTM! Workspace creation tracked with full context.

The workspace_created event includes comprehensive properties (workspace_id, project_id, branch, base_branch) for detailed telemetry analysis.


282-287: LGTM! Branch workspace opened tracked correctly.

The event includes the was_existing flag to distinguish between creating a new workspace and reactivating an existing one.


790-790: LGTM! Workspace lifecycle events tracked.

Both workspace_deleted and workspace_closed events are tracked after successful operations, providing visibility into workspace management.

Also applies to: 1136-1136

apps/desktop/src/main/lib/analytics/index.ts (3)

23-32: LGTM! User ID caching with graceful error handling.

The async fetch with caching pattern is appropriate for desktop analytics. Swallowing errors ensures tracking failures don't impact the user experience.


45-66: LGTM! Fire-and-forget tracking pattern.

The implementation correctly follows the fire-and-forget pattern for analytics, ensuring tracking never blocks the UI or impacts app functionality. The automatic addition of app_name and platform provides consistent context across all events.


71-73: LGTM! Graceful shutdown handling.

The optional chaining safely handles the case where PostHog was never initialized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 22, 2025

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

Service Status
Neon Database (Neon)

Thank you for your contribution! 🎉


Preview resources have been processed for cleanup

Copy link
Copy Markdown
Contributor

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest risk is analytics correctness/reliability: several critical events can be silently dropped due to track() requiring user.me to succeed (notably auth_completed) and errors being fully swallowed in main/lib/analytics. Desktop’s desktop_opened is currently tied to the sign-in screen, which will undercount real launches. In web/admin, the move to user.me is good, but the identify/reset logic should be keyed to query completion to avoid subtle session-transition edge cases. Finally, workspace event payloads should be normalized across code paths to keep PostHog schemas consistent.

Additional notes (5)
  • Readability | apps/desktop/src/main/index.ts:49-54
    track("auth_completed") fires immediately after handleAuthCallback resolves, but there’s no guarantee the user profile (user.me) is available yet for main/lib/analytics to attach a distinctId. As implemented, track() silently drops the event if getUserId() returns null.

That means a meaningful portion of auth_completed events could be lost (especially on slow networks / first app launch) and you would never know because errors are swallowed.

  • Maintainability | apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts:167-179
    track("workspace_created") is emitted after loadSetupConfig(project.mainRepoPath). If loadSetupConfig throws (or future changes add failure paths after the workspace is created but before returning), you’ll miss the creation event even though the workspace exists.

For analytics correctness, events that describe side effects (workspace created) should be emitted immediately after the side effect is committed (DB write), not after unrelated work.

  • Maintainability | apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts:279-279
    track("workspace_opened") in the branch workspace flow includes was_existing, which is good, but the worktree flow’s workspace_opened event does not. That makes the event schema inconsistent for the same event name and complicates analysis downstream.

Similarly, workspace_closed and workspace_deleted only include workspace_id, which may be insufficient for segmentation (project, workspace type, etc.) if you need it later.

  • Maintainability | apps/desktop/src/main/lib/terminal/manager.ts:59-59
    terminal_opened is fired in doCreateSession and includes workspace_id, but the previous code path didn’t require that workspaceId is present. If workspaceId can be missing/empty in some flows (e.g., ad-hoc terminals), you’ll either send workspace_id: undefined or lose the ability to attribute sessions.

Given the rest of your desktop analytics is workspace-centric, consider either enforcing that workspaceId is always present for session creation, or intentionally omitting it when unavailable.

  • Maintainability | apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx:23-28
    The marketing DownloadButton now imports posthog-js directly and captures events. This assumes PostHog is always initialized in this bundle. If initialization is conditional (env-based) or delayed, posthog.capture can become a no-op or behave unexpectedly. Also, if this component is used before PostHog init finishes, events might be lost.

Since this PR adds many events, a centralized wrapper (that checks init and can buffer) reduces future drift across apps.

Summary of changes

What changed

Cross-app user identity normalization

  • Updated Admin and Web PostHogUserIdentifier components to identify users via the backend trpc.user.me (@tanstack/react-query + useTRPC) instead of using Clerk’s user.id.
  • Updated Desktop Renderer PostHogUserIdentifier to reset PostHog only when the user.me query is successful and returns no user.

Desktop analytics instrumentation

  • Added a new main-process analytics module at apps/desktop/src/main/lib/analytics/index.ts using posthog-node to reliably capture events server-side.
  • Added posthog-node dependency to the desktop app.
  • Instrumented Desktop events:
    • auth_completed after OAuth callback processing in apps/desktop/src/main/index.ts
    • Workspace lifecycle events (workspace_created, workspace_opened, workspace_closed, workspace_deleted) in apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
    • terminal_opened when creating terminal sessions in apps/desktop/src/main/lib/terminal/manager.ts
  • Added desktop env vars NEXT_PUBLIC_POSTHOG_KEY (optional) and NEXT_PUBLIC_POSTHOG_HOST (defaulted) to apps/desktop/src/main/env.main.ts.
  • Ensured analytics cache is cleared on desktop sign out via clearUserCache().

Marketing analytics

  • Added download_clicked and waitlist_clicked PostHog captures to apps/marketing/src/app/components/DownloadButton/DownloadButton.tsx.

Test support

  • Extended apps/desktop/test-setup.ts to mock electron.shell.openExternal and main/lib/analytics for tests.

Comment on lines 9 to +24
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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posthog.reset() is gated on isSignedIn === false, but not on the me query being finished. If the query is still in-flight and user is undefined, this effect currently does nothing (good), but when isSignedIn flips to false you reset immediately even if there’s still cached React Query data for user.me (or a stale user value). 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:

  • Destructure isSuccess/isFetched (or status) from useQuery.
  • Only call posthog.identify when isSuccess && user.
  • Only call posthog.reset when isSuccess && !user or when isSignedIn === false and the query has settled.

Example:

const { data: user, isSuccess } = useQuery({
  ...trpc.user.me.queryOptions(),
  enabled: isSignedIn,
});

useEffect(() => {
  if (!isSuccess) return;
  if (user) posthog.identify(user.id, { email: user.email, name: user.name });
  else posthog.reset();
}, [isSuccess, user]);

Reply with "@CharlieHelps yes please" if you’d like me to add a commit implementing this change.

Comment on lines 9 to +24
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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as in admin: the reset behavior depends on isSignedIn === false rather than the user.me query lifecycle. Without keying off query completion, you can get subtle timing issues during sign-out/session refresh where PostHog identity is not deterministically cleared based on the authoritative backend state.

Suggestion

Align reset/identify logic to the React Query result status (e.g., isSuccess/isFetched) rather than isSignedIn alone, so the component behavior is driven by the same authoritative source (user.me) you introduced.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit applying the same pattern as suggested for admin.

Comment on lines +1 to +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,
});
}
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();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analytics module drops events whenever getUserId() fails, and it never flushes/awaits delivery for important lifecycle events. With flushAt: 1 and flushInterval: 0 you are aiming for immediate delivery, but without awaiting capture / explicit flush, events can still be lost on shutdown.

Also, swallowing all errors (catch(() => {})) removes any ability to detect a misconfiguration (bad key/host) in production logs.

Suggestion

Improve reliability/observability while keeping it low-noise:

  1. For shutdown, explicitly await client?.flush() before shutdown() (or ensure shutdown() flushes in your posthog-node version).
  2. For track, consider capturing events even without a user via an app-scoped distinct id (e.g., a persisted installation id) so you don’t lose pre-auth events.
  3. Replace the blanket catch(() => {}) with a very low-volume logger gated by NODE_ENV !== 'test' and maybe a simple once-per-session guard.

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).

Comment on lines +13 to +19
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,
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posthog-node is configured with flushAt: 1 and flushInterval: 0, which can create very chatty network usage (each event becomes its own request) and may impact performance/battery on laptops, especially given workspace/terminal events can be frequent. Also, shutdown() only runs on before-quit; crashes/forced exits will drop events anyway, so paying the cost of per-event flush may not be worth it.

Suggestion

Use a small batch/interval to reduce overhead (e.g., flushAt: 5 and flushInterval: 5000) and rely on shutdown() to flush on graceful exit. Optionally call client?.flush() after high-value events (auth completed) if needed.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit adjusting the PostHog client config and adding an optional flush() helper for critical moments.

Comment on lines 167 to +176
// 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,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

track("workspace_created"/"workspace_opened") is called unconditionally after workspace creation/open. If any downstream logic throws after the DB update but before returning (e.g., config loading, other steps not shown), you may emit events for operations that ultimately fail from the user's perspective. This can pollute funnels and make metrics hard to trust.

Suggestion

Consider 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 try/catch and emit separate failure events (e.g. workspace_create_failed) when exceptions occur.

Example pattern:

  • Do all work
  • return result
  • In a final step right before return, call track(...)

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor across the workspace mutations.

Comment on lines +79 to +81
// Track terminal opened (only fires once per session creation)
track("terminal_opened", { workspace_id: workspaceId, pane_id: paneId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

track("terminal_opened") runs for every session creation, including potentially during recovery flows depending on how createSession/pendingSessions behave. If recovered sessions are common, this may overcount. Also, workspace_id comes from params; ensure it’s always present/meaningful for analytics (otherwise you'll get a lot of null/undefined).

Suggestion

Gate the event to truly-new sessions only (e.g., if (!session.wasRecovered) track(...)) or include was_recovered: session.wasRecovered so analysis can filter accurately.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that includes was_recovered (or guards tracking) here.

Comment on lines 11 to 22
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 });
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

desktop_opened is being captured in the Sign In screen mount effect. That likely undercounts launches where the user is already authenticated and never hits this screen, and over-couples an app-lifecycle event to a specific UI route.

Since you already introduced a main-process analytics module, app-open should be tracked from the main process (e.g., when app.whenReady() resolves), potentially with a separate renderer event for “sign_in_screen_viewed” if you want it.

Suggestion

Move desktop_opened to the main process (e.g., right after the app is ready / window created) using main/lib/analytics.track, and replace the renderer event with a screen-specific event if desired.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant