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