diff --git a/web/app/(commonLayout)/external-service-sync.tsx b/web/app/(commonLayout)/external-service-sync.tsx index a50dc3383cf..409e03d9231 100644 --- a/web/app/(commonLayout)/external-service-sync.tsx +++ b/web/app/(commonLayout)/external-service-sync.tsx @@ -1,13 +1,70 @@ 'use client' +import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' +import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen' +import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' +import { useEffect, useRef } from 'react' +import { setUserId, setUserProperties } from '@/app/components/base/amplitude' +import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' import { useAmplitudeInitialized } from '@/app/components/base/amplitude/use-amplitude-initialized' import { useAnalyticsConsent } from '@/app/components/base/analytics-consent/consent-store' -import { amplitudeIdentitySyncAtom } from '@/context/amplitude-identity-sync' +import { currentWorkspaceAtom } from '@/context/workspace-state' import { zendeskConversationSyncAtom } from '@/context/zendesk-conversation-sync' +import { userProfileQueryOptions } from '@/features/account-profile/client' + +type AmplitudeProperties = Record + +function buildAmplitudeProperties({ + currentWorkspace, + userProfile, +}: { + currentWorkspace: GetWorkspacesCurrentSummaryResponse + userProfile: GetAccountProfileResponse +}) { + const properties: AmplitudeProperties = { + email: userProfile.email, + name: userProfile.name, + has_password: userProfile.is_password_set, + } + + if (currentWorkspace.id) { + properties.workspace_id = currentWorkspace.id + properties.workspace_name = currentWorkspace.name + if (currentWorkspace.plan) properties.workspace_plan = currentWorkspace.plan + properties.workspace_role = currentWorkspace.role + } + + return properties +} function AmplitudeIdentitySync() { - useAtomValue(amplitudeIdentitySyncAtom) + const { data: userProfile } = useSuspenseQuery({ + ...userProfileQueryOptions(), + select: (data) => data.profile, + }) + const currentWorkspace = useAtomValue(currentWorkspaceAtom) + const lastIdentityRef = useRef(undefined) + + useEffect(() => { + if (!userProfile.id) return + + const properties = buildAmplitudeProperties({ + currentWorkspace, + userProfile, + }) + const identity = JSON.stringify({ + userId: userProfile.email, + properties, + }) + + if (identity === lastIdentityRef.current) return + + setUserId(userProfile.email) + setUserProperties(properties) + flushRegistrationSuccess() + lastIdentityRef.current = identity + }, [currentWorkspace, userProfile]) return null } diff --git a/web/context/__tests__/console-bootstrap.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx index 796db50e601..58dfc291a5d 100644 --- a/web/context/__tests__/console-bootstrap.spec.tsx +++ b/web/context/__tests__/console-bootstrap.spec.tsx @@ -565,5 +565,42 @@ describe('Console bootstrap', () => { expect(flushRegistrationSuccess).toHaveBeenCalled() }) }) + + it('should resync Amplitude only when identity properties change', async () => { + const { queryClient, rerender } = renderConsoleBootstrap() + + await waitFor(() => expect(setUserProperties).toHaveBeenCalledTimes(1)) + + rerender( + + + + loading}> + + + + + + , + ) + expect(setUserProperties).toHaveBeenCalledTimes(1) + + act(() => { + queryClient.setQueryData(['user-profile'], { + ...mockUserProfileResponseState.data, + profile: { + ...mockUserProfileResponseState.data.profile, + name: 'Updated User', + }, + }) + }) + + await waitFor(() => { + expect(setUserProperties).toHaveBeenCalledTimes(2) + expect(setUserProperties).toHaveBeenLastCalledWith( + expect.objectContaining({ name: 'Updated User' }), + ) + }) + }) }) }) diff --git a/web/context/amplitude-identity-sync.ts b/web/context/amplitude-identity-sync.ts deleted file mode 100644 index bbdb3c8e23c..00000000000 --- a/web/context/amplitude-identity-sync.ts +++ /dev/null @@ -1,60 +0,0 @@ -'use client' - -import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' -import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen' -import { atom } from 'jotai' -import { atomEffect } from 'jotai-effect' -import { setUserId, setUserProperties } from '@/app/components/base/amplitude' -import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' -import { userProfileAtom } from './account-state' -import { currentWorkspaceAtom } from './workspace-state' - -type AmplitudeProperties = Record - -const amplitudeIdentityAtom = atom(undefined) - -function buildAmplitudeProperties({ - currentWorkspace, - userProfile, -}: { - currentWorkspace: GetWorkspacesCurrentSummaryResponse - userProfile: GetAccountProfileResponse -}) { - const properties: AmplitudeProperties = { - email: userProfile.email, - name: userProfile.name, - has_password: userProfile.is_password_set, - } - - if (currentWorkspace.id) { - properties.workspace_id = currentWorkspace.id - properties.workspace_name = currentWorkspace.name - if (currentWorkspace.plan) properties.workspace_plan = currentWorkspace.plan - properties.workspace_role = currentWorkspace.role - } - - return properties -} - -export const amplitudeIdentitySyncAtom = atomEffect((get, set) => { - const userProfile = get(userProfileAtom) - const currentWorkspace = get(currentWorkspaceAtom) - - if (!userProfile.id) return - - const properties = buildAmplitudeProperties({ - currentWorkspace, - userProfile, - }) - const identity = JSON.stringify({ - userId: userProfile.email, - properties, - }) - - if (identity === get.peek(amplitudeIdentityAtom)) return - - setUserId(userProfile.email) - setUserProperties(properties) - flushRegistrationSuccess() - set(amplitudeIdentityAtom, identity) -})