From de176ec4583ff69e942a6eb22a37be44475b8637 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:21:37 +0530 Subject: [PATCH] identify signed-in users on every app startup Previously identify() only fired during the OAuth completion flow, so existing installs (signed in before analytics shipped) and every cold start of v0.3.4+ would emit main-process events under the anonymous installation_id until the user happened to re-sign-in. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/x/ANALYTICS.md | 1 + apps/x/apps/main/src/main.ts | 8 +++++++ .../x/packages/core/src/analytics/identify.ts | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 apps/x/packages/core/src/analytics/identify.ts diff --git a/apps/x/ANALYTICS.md b/apps/x/ANALYTICS.md index 04659952..f0372dd5 100644 --- a/apps/x/ANALYTICS.md +++ b/apps/x/ANALYTICS.md @@ -10,6 +10,7 @@ - Main does it from `apps/main/src/oauth-handler.ts:285` (after `getBillingInfo()` resolves) — this is the load-bearing call, since main always runs. - Renderer mirrors via `apps/renderer/src/hooks/useAnalyticsIdentity.ts` listening on the `oauth:didConnect` IPC event. - Main also calls `alias()` so events emitted under the anonymous installation_id are linked to the identified user retroactively. +- **On every app startup**: main re-identifies if rowboat tokens exist (`packages/core/src/analytics/identify.ts`, called from `apps/main/src/main.ts` whenReady). Idempotent — PostHog merges person properties on duplicate identifies. This catches users who installed before analytics existed, and refreshes person properties (plan/status) on every launch. - **On rowboat sign-out**: `posthog.reset()` in both processes; future events resolve to the installation_id again. - **`email`** is set on `identify` from main only (sourced from `/v1/me`). Person properties are server-side, so the renderer's events resolve to the same record without redundantly setting it. diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index f04a0ecc..99c77589 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -27,6 +27,7 @@ import { init as initTrackScheduler } from "@x/core/dist/knowledge/track/schedul import { init as initTrackEventProcessor } from "@x/core/dist/knowledge/track/events.js"; import { init as initLocalSites, shutdown as shutdownLocalSites } from "@x/core/dist/local-sites/server.js"; import { shutdown as shutdownAnalytics } from "@x/core/dist/analytics/posthog.js"; +import { identifyIfSignedIn } from "@x/core/dist/analytics/identify.js"; import { initConfigs } from "@x/core/dist/config/initConfigs.js"; import started from "electron-squirrel-startup"; @@ -231,6 +232,13 @@ app.whenReady().then(async () => { // Initialize all config files before UI can access them await initConfigs(); + // PostHog identify() is idempotent — call it on every startup so existing + // signed-in installs (and every cold start of v0.3.4+) get re-identified. + // Otherwise main-process events stay anonymous until the user re-signs-in. + identifyIfSignedIn().catch((error) => { + console.error('[Analytics] Failed to identify on startup:', error); + }); + registerBrowserControlService(new ElectronBrowserControlService()); setupIpcHandlers(); diff --git a/apps/x/packages/core/src/analytics/identify.ts b/apps/x/packages/core/src/analytics/identify.ts new file mode 100644 index 00000000..3d647711 --- /dev/null +++ b/apps/x/packages/core/src/analytics/identify.ts @@ -0,0 +1,23 @@ +import { isSignedIn } from '../account/account.js'; +import { getBillingInfo } from '../billing/billing.js'; +import { identify } from './posthog.js'; + +/** + * If the user has rowboat OAuth tokens, fetch their billing info and + * call posthog.identify(). Idempotent — safe to call on every app start. + * Catches all errors so analytics never blocks app launch. + */ +export async function identifyIfSignedIn(): Promise { + try { + if (!(await isSignedIn())) return; + const billing = await getBillingInfo(); + if (!billing.userId) return; + identify(billing.userId, { + ...(billing.userEmail ? { email: billing.userEmail } : {}), + plan: billing.subscriptionPlan, + status: billing.subscriptionStatus, + }); + } catch (err) { + console.error('[Analytics] startup identify failed:', err); + } +}