From 71936a69da27133dac380d2a4e88331bdf16f88c Mon Sep 17 00:00:00 2001 From: Sean Oliver <882952+seanoliver@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:53:06 -0800 Subject: [PATCH] fix(telemetry): pass PostHog distinct_id in identify request (#41322) --- packages/api-types/types/platform.d.ts | 2 ++ packages/common/posthog-client.ts | 26 +++++++++++++++++++++++--- packages/common/telemetry.tsx | 5 +++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 5b1dfd39b30..5e1cc1f73fa 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -9218,6 +9218,8 @@ export interface components { reset_project?: boolean } TelemetryIdentifyBodyV2: { + /** PostHog JS SDK's distinct_id - used for aliasing anonymous → authenticated users */ + anonymous_id?: string organization_slug?: string project_ref?: string user_id: string diff --git a/packages/common/posthog-client.ts b/packages/common/posthog-client.ts index 1ce1a8f5ad9..042cf62e439 100644 --- a/packages/common/posthog-client.ts +++ b/packages/common/posthog-client.ts @@ -11,6 +11,9 @@ interface PostHogClientConfig { } class PostHogClient { + /** True after posthog.init() is called (prevents double-init) */ + private initStarted = false + /** True after the `loaded` callback fires, meaning PostHog has fully bootstrapped */ private initialized = false private pendingGroups: Record = {} private pendingIdentification: { userId: string; properties?: Record } | null = null @@ -27,7 +30,7 @@ class PostHogClient { } init(hasConsent: boolean = true) { - if (this.initialized || typeof window === 'undefined' || !hasConsent) return + if (this.initStarted || typeof window === 'undefined' || !hasConsent) return if (!this.config.apiKey) { console.warn('PostHog API key not found. Skipping initialization.') @@ -72,11 +75,13 @@ class PostHogClient { } }) this.pendingEvents = [] + + this.initialized = true }, } + this.initStarted = true posthog.init(this.config.apiKey, config) - this.initialized = true } capturePageView(properties: Record, hasConsent: boolean = true) { @@ -148,7 +153,7 @@ class PostHogClient { this.pendingGroups = {} this.pendingEvents = [] - if (!this.initialized) return + if (!this.initStarted) return try { posthog.reset() @@ -156,6 +161,21 @@ class PostHogClient { console.error('PostHog reset failed:', error) } } + + /** + * Returns PostHog's distinct_id, which holds first-touch attribution data. + * Returns undefined until PostHog's `loaded` callback fires. + */ + getDistinctId(): string | undefined { + if (!this.initialized) return undefined + + try { + return posthog.get_distinct_id() + } catch (error) { + console.error('PostHog getDistinctId failed:', error) + return undefined + } + } } export const posthogClient = new PostHogClient() diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 58290d6cbee..42f3385322e 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -320,12 +320,13 @@ export function useTelemetryIdentify(API_URL: string) { useEffect(() => { if (user?.id) { - // Send to backend + const anonymousId = posthogClient.getDistinctId() + sendTelemetryIdentify(API_URL, { user_id: user.id, + ...(anonymousId && { anonymous_id: anonymousId }), }) - // Also identify in PostHog client-side posthogClient.identify(user.id) } }, [API_URL, user?.id])