mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
refactor(web): sync app context effects with jotai (#38570)
This commit is contained in:
parent
9416d04163
commit
caf1a22020
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -405,6 +405,9 @@ catalogs:
|
||||
jotai:
|
||||
specifier: 2.20.1
|
||||
version: 2.20.1
|
||||
jotai-effect:
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1
|
||||
jotai-scope:
|
||||
specifier: 0.11.0
|
||||
version: 0.11.0
|
||||
@ -1252,6 +1255,9 @@ importers:
|
||||
jotai:
|
||||
specifier: 'catalog:'
|
||||
version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
|
||||
jotai-effect:
|
||||
specifier: 'catalog:'
|
||||
version: 2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))
|
||||
jotai-scope:
|
||||
specifier: 'catalog:'
|
||||
version: 0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)
|
||||
@ -7364,6 +7370,12 @@ packages:
|
||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||
hasBin: true
|
||||
|
||||
jotai-effect@2.3.1:
|
||||
resolution: {integrity: sha512-FBBVXDM2podbbxJsZ19+uDv45LxeXoVA5yh6CfkQ35AVCuRHj7Lanlcjiea3b67Q7+/MMGXWf8+GeB73JcbeMg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
jotai: '>=2.20.0'
|
||||
|
||||
jotai-scope@0.11.0:
|
||||
resolution: {integrity: sha512-ofiW0Z0i3lTw509Gx0+T6fqsDPMDxMn+AHmNs9iF9OA8CmK1/0xRprPxuZ89UZdBzt6jcrTYdunNZSF2255zJQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -16663,6 +16675,10 @@ snapshots:
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
jotai-effect@2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)):
|
||||
dependencies:
|
||||
jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
|
||||
|
||||
jotai-scope@0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
|
||||
@ -20034,6 +20050,7 @@ time:
|
||||
i18next@26.3.3: '2026-06-26T11:23:58.220Z'
|
||||
iconify-import-svg@0.2.0: '2026-04-20T06:18:25.132Z'
|
||||
immer@11.1.8: '2026-05-08T15:09:33.021Z'
|
||||
jotai-effect@2.3.1: '2026-05-13T18:51:15.911Z'
|
||||
jotai-scope@0.11.0: '2026-05-13T18:43:15.331Z'
|
||||
jotai-tanstack-query@0.11.0: '2025-08-01T02:55:49.826Z'
|
||||
jotai@2.20.1: '2026-06-11T06:30:45.782Z'
|
||||
|
||||
@ -185,6 +185,7 @@ catalog:
|
||||
iconify-import-svg: 0.2.0
|
||||
immer: 11.1.8
|
||||
jotai: 2.20.1
|
||||
jotai-effect: 2.3.1
|
||||
jotai-scope: 0.11.0
|
||||
jotai-tanstack-query: 0.11.0
|
||||
js-audio-recorder: 1.0.7
|
||||
|
||||
12
web/app/(commonLayout)/external-service-sync.tsx
Normal file
12
web/app/(commonLayout)/external-service-sync.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { amplitudeIdentitySyncAtom } from '@/context/amplitude-identity-sync'
|
||||
import { zendeskConversationSyncAtom } from '@/context/zendesk-conversation-sync'
|
||||
|
||||
export function ExternalServiceSync() {
|
||||
useAtomValue(zendeskConversationSyncAtom)
|
||||
useAtomValue(amplitudeIdentitySyncAtom)
|
||||
|
||||
return null
|
||||
}
|
||||
@ -61,28 +61,22 @@ export async function CommonLayoutHydrationBoundary({ children }: { children: Re
|
||||
const queryClient = getQueryClientServer()
|
||||
const accountProfileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH)
|
||||
|
||||
if (!accountProfileUrl) {
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
{children}
|
||||
</HydrationBoundary>
|
||||
)
|
||||
}
|
||||
if (accountProfileUrl) {
|
||||
try {
|
||||
const context = await getServerConsoleClientContext()
|
||||
|
||||
try {
|
||||
const context = await getServerConsoleClientContext()
|
||||
|
||||
await Promise.all([
|
||||
queryClient.fetchQuery(serverUserProfileQueryOptions()),
|
||||
queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()),
|
||||
queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({
|
||||
context,
|
||||
retry: false,
|
||||
})),
|
||||
])
|
||||
}
|
||||
catch (error) {
|
||||
await handleProfileError(error)
|
||||
await Promise.all([
|
||||
queryClient.fetchQuery(serverUserProfileQueryOptions()),
|
||||
queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()),
|
||||
queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({
|
||||
context,
|
||||
retry: false,
|
||||
})),
|
||||
])
|
||||
}
|
||||
catch (error) {
|
||||
await handleProfileError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
|
||||
import Zendesk from '@/app/components/base/zendesk'
|
||||
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
|
||||
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
|
||||
import MainNavLayout from '@/app/components/main-nav/layout'
|
||||
import { NextRouteStateBridge } from '@/app/components/next-route-state'
|
||||
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
|
||||
import { AppBootstrapEffects } from '@/context/app-bootstrap-effects'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||
import { CommonLayoutGlobalMounts } from './global-mounts'
|
||||
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
|
||||
import { ConsoleContextProviders, ConsoleRuntimeProviders } from './providers'
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
@ -23,28 +15,19 @@ export default async function Layout({
|
||||
}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GoogleAnalyticsScripts />
|
||||
<AmplitudeProvider />
|
||||
<OAuthRegistrationAnalytics />
|
||||
<EducationVerifyActionRecorder />
|
||||
<CommonLayoutHydrationBoundary>
|
||||
<ConsoleRuntimeProviders>
|
||||
<NextRouteStateBridge>
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<MaintenanceNotice />
|
||||
<AppBootstrapEffects />
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<MainNavLayout detailSidebar={detailSidebar}>
|
||||
{children}
|
||||
</MainNavLayout>
|
||||
<CommonLayoutGlobalMounts />
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
<ConsoleContextProviders>
|
||||
<MainNavLayout detailSidebar={detailSidebar}>
|
||||
{children}
|
||||
</MainNavLayout>
|
||||
<CommonLayoutGlobalMounts />
|
||||
</ConsoleContextProviders>
|
||||
</div>
|
||||
</NextRouteStateBridge>
|
||||
</CommonLayoutHydrationBoundary>
|
||||
</ConsoleRuntimeProviders>
|
||||
<Zendesk />
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
37
web/app/(commonLayout)/providers.tsx
Normal file
37
web/app/(commonLayout)/providers.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
|
||||
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
|
||||
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||
import { ExternalServiceSync } from './external-service-sync'
|
||||
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
|
||||
|
||||
export async function ConsoleRuntimeProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<GoogleAnalyticsScripts />
|
||||
<AmplitudeProvider />
|
||||
<OAuthRegistrationAnalytics />
|
||||
<EducationVerifyActionRecorder />
|
||||
<CommonLayoutHydrationBoundary>
|
||||
<ExternalServiceSync />
|
||||
{children}
|
||||
</CommonLayoutHydrationBoundary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConsoleContextProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
{children}
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
)
|
||||
}
|
||||
@ -1,42 +1,25 @@
|
||||
import * as React from 'react'
|
||||
import { CommonLayoutHydrationBoundary } from '@/app/(commonLayout)/hydration-boundary'
|
||||
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||
import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
|
||||
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
|
||||
import { ConsoleContextProviders, ConsoleRuntimeProviders } from '@/app/(commonLayout)/providers'
|
||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
|
||||
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
|
||||
import { AppBootstrapEffects } from '@/context/app-bootstrap-effects'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||
import Header from './header'
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GoogleAnalyticsScripts />
|
||||
<AmplitudeProvider />
|
||||
<OAuthRegistrationAnalytics />
|
||||
<EducationVerifyActionRecorder />
|
||||
<CommonLayoutHydrationBoundary>
|
||||
<ConsoleRuntimeProviders>
|
||||
<div className="flex h-full flex-col overflow-hidden bg-background-body">
|
||||
<MaintenanceNotice />
|
||||
<AppBootstrapEffects />
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<div className="relative flex h-0 min-h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg">
|
||||
{children}
|
||||
</div>
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
<ConsoleContextProviders>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
<div className="relative flex h-0 min-h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg">
|
||||
{children}
|
||||
</div>
|
||||
</ConsoleContextProviders>
|
||||
</div>
|
||||
</CommonLayoutHydrationBoundary>
|
||||
</ConsoleRuntimeProviders>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@ -48,8 +48,8 @@ export function OAuthRegistrationAnalytics() {
|
||||
|
||||
const eventName = utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success'
|
||||
|
||||
// Defer the Amplitude event until the user ID is attached. It is flushed in
|
||||
// AppBootstrapEffects after setUserId runs. Firing it here would record it under an
|
||||
// Defer the Amplitude event until the user ID is attached. The app context
|
||||
// external sync replays it after setUserId runs. Firing it here would record it under an
|
||||
// anonymous Amplitude profile (no user ID set yet).
|
||||
rememberRegistrationSuccess({ method: 'oauth', utmInfo })
|
||||
|
||||
|
||||
@ -78,8 +78,8 @@ const ChangePasswordForm = () => {
|
||||
if (result === 'success') {
|
||||
const utmInfo = parseUtmInfo()
|
||||
rememberCreateAppExternalAttribution({ utmInfo })
|
||||
// Defer the Amplitude event until the user ID is attached. It is flushed in
|
||||
// AppBootstrapEffects after setUserId runs once the redirect lands on /apps.
|
||||
// Defer the Amplitude event until the user ID is attached. The app context
|
||||
// external sync replays it after setUserId runs once the redirect lands on /apps.
|
||||
// Firing it here would record it under an anonymous Amplitude profile.
|
||||
rememberRegistrationSuccess({ method: 'email', utmInfo })
|
||||
|
||||
|
||||
@ -5,11 +5,11 @@ import { Provider as JotaiProvider, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { queryClientAtom } from 'jotai-tanstack-query'
|
||||
import { useHydrateAtoms } from 'jotai/react/utils'
|
||||
import { Suspense } from 'react'
|
||||
import { ExternalServiceSync } from '@/app/(commonLayout)/external-service-sync'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { AppBootstrapEffects } from '../app-bootstrap-effects'
|
||||
import { initialWorkspaceInfo } from '../app-context-defaults'
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
@ -167,7 +167,7 @@ vi.mock('@/app/components/header/maintenance-notice', () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
function AppContextProbe() {
|
||||
function ConsoleBootstrapProbe() {
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const roleFlags = useAtomValue(workspaceRoleFlagsAtom)
|
||||
@ -257,15 +257,15 @@ function createTestQueryClient() {
|
||||
})
|
||||
}
|
||||
|
||||
function renderBootstrapEffects() {
|
||||
function renderConsoleBootstrap() {
|
||||
const queryClient = createTestQueryClient()
|
||||
const view = render(
|
||||
<JotaiProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TestQueryClientHydrator queryClient={queryClient}>
|
||||
<Suspense fallback={<span>loading</span>}>
|
||||
<AppBootstrapEffects />
|
||||
<AppContextProbe />
|
||||
<ExternalServiceSync />
|
||||
<ConsoleBootstrapProbe />
|
||||
</Suspense>
|
||||
</TestQueryClientHydrator>
|
||||
</QueryClientProvider>
|
||||
@ -278,7 +278,7 @@ function renderBootstrapEffects() {
|
||||
}
|
||||
}
|
||||
|
||||
describe('AppBootstrapEffects', () => {
|
||||
describe('Console bootstrap', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPermissionKeysState.isPending = false
|
||||
@ -339,7 +339,7 @@ describe('AppBootstrapEffects', () => {
|
||||
|
||||
describe('Bootstrap atoms', () => {
|
||||
it('should provide profile, workspace, permissions, loading state, and version metadata', async () => {
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText('user:user@example.com')).toBeInTheDocument()
|
||||
expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument()
|
||||
@ -360,7 +360,7 @@ describe('AppBootstrapEffects', () => {
|
||||
mockPermissionKeysState.permissionKeys = []
|
||||
mockLangGeniusVersionState.data = undefined
|
||||
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText('user:')).toBeInTheDocument()
|
||||
expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument()
|
||||
@ -375,7 +375,7 @@ describe('AppBootstrapEffects', () => {
|
||||
role: 'unsupported-role',
|
||||
}
|
||||
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
|
||||
})
|
||||
@ -386,7 +386,7 @@ describe('AppBootstrapEffects', () => {
|
||||
role: 'owner',
|
||||
}
|
||||
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText('manager:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('owner:true')).toBeInTheDocument()
|
||||
@ -398,7 +398,7 @@ describe('AppBootstrapEffects', () => {
|
||||
mockPermissionKeysState.isPending = true
|
||||
mockCurrentWorkspaceQueryState.isPending = true
|
||||
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
expect(await screen.findByText('workspace loading:true')).toBeInTheDocument()
|
||||
expect(screen.getByText('permission loading:true')).toBeInTheDocument()
|
||||
@ -407,7 +407,7 @@ describe('AppBootstrapEffects', () => {
|
||||
|
||||
describe('Refresh actions', () => {
|
||||
it('should invalidate the source queries when refresh actions are called', async () => {
|
||||
const { queryClient } = renderBootstrapEffects()
|
||||
const { queryClient } = renderConsoleBootstrap()
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /refresh user/i }))
|
||||
@ -420,7 +420,7 @@ describe('AppBootstrapEffects', () => {
|
||||
|
||||
describe('External side effects', () => {
|
||||
it('should sync Zendesk fields and Amplitude identity when bootstrap data is available', async () => {
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
|
||||
@ -469,7 +469,7 @@ describe('AppBootstrapEffects', () => {
|
||||
},
|
||||
}
|
||||
|
||||
renderBootstrapEffects()
|
||||
renderConsoleBootstrap()
|
||||
|
||||
await screen.findByText('user:')
|
||||
expect(setUserId).not.toHaveBeenCalled()
|
||||
65
web/context/amplitude-identity-sync.ts
Normal file
65
web/context/amplitude-identity-sync.ts
Normal file
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
|
||||
import type { ICurrentWorkspace } from '@/models/common'
|
||||
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 {
|
||||
currentWorkspaceAtom,
|
||||
userProfileAtom,
|
||||
} from './app-context-state'
|
||||
|
||||
type AmplitudeProperties = Record<string, string | number | boolean>
|
||||
|
||||
const amplitudeIdentityAtom = atom<string | undefined>(undefined)
|
||||
|
||||
function buildAmplitudeProperties({
|
||||
currentWorkspace,
|
||||
userProfile,
|
||||
}: {
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
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
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_status = currentWorkspace.status
|
||||
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)
|
||||
})
|
||||
@ -1,13 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
useSyncAmplitudeIdentity,
|
||||
useSyncZendeskFields,
|
||||
} from './app-context-effects'
|
||||
|
||||
export function AppBootstrapEffects() {
|
||||
useSyncZendeskFields()
|
||||
useSyncAmplitudeIdentity()
|
||||
|
||||
return null
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect } from 'react'
|
||||
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
langGeniusVersionInfoAtom,
|
||||
userProfileAtom,
|
||||
} from './app-context-state'
|
||||
|
||||
export function useSyncZendeskFields() {
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: langGeniusVersionInfo.current_env.toLowerCase(),
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.current_env])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: langGeniusVersionInfo.version,
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.version])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: userProfile.email,
|
||||
}])
|
||||
}
|
||||
}, [userProfile?.email])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: currentWorkspace.id,
|
||||
}])
|
||||
}
|
||||
}, [currentWorkspace?.id])
|
||||
}
|
||||
|
||||
export function useSyncAmplitudeIdentity() {
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile?.id) {
|
||||
setUserId(userProfile.email)
|
||||
const properties: Record<string, string | number | boolean> = {
|
||||
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
|
||||
properties.workspace_plan = currentWorkspace.plan
|
||||
properties.workspace_status = currentWorkspace.status
|
||||
properties.workspace_role = currentWorkspace.role
|
||||
}
|
||||
|
||||
setUserProperties(properties)
|
||||
flushRegistrationSuccess()
|
||||
}
|
||||
}, [userProfile, currentWorkspace])
|
||||
}
|
||||
88
web/context/zendesk-conversation-sync.ts
Normal file
88
web/context/zendesk-conversation-sync.ts
Normal file
@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { atom } from 'jotai'
|
||||
import { atomEffect } from 'jotai-effect'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
langGeniusVersionInfoAtom,
|
||||
userProfileAtom,
|
||||
} from './app-context-state'
|
||||
|
||||
type ZendeskSyncState = {
|
||||
email?: string
|
||||
environment?: string
|
||||
version?: string
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
const zendeskConversationSyncStateAtom = atom<ZendeskSyncState>({})
|
||||
|
||||
function syncZendeskField({
|
||||
fieldId,
|
||||
previousValue,
|
||||
setNextValue,
|
||||
value,
|
||||
}: {
|
||||
fieldId: string | undefined
|
||||
previousValue: string | undefined
|
||||
setNextValue: (value: string) => void
|
||||
value: string
|
||||
}) {
|
||||
if (!fieldId || !value || value === previousValue)
|
||||
return false
|
||||
|
||||
setZendeskConversationFields([{
|
||||
id: fieldId,
|
||||
value,
|
||||
}])
|
||||
setNextValue(value)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const zendeskConversationSyncAtom = atomEffect((get, set) => {
|
||||
const userProfile = get(userProfileAtom)
|
||||
const currentWorkspace = get(currentWorkspaceAtom)
|
||||
const langGeniusVersionInfo = get(langGeniusVersionInfoAtom)
|
||||
const state = get.peek(zendeskConversationSyncStateAtom)
|
||||
const nextState = { ...state }
|
||||
|
||||
let didSync = false
|
||||
didSync = syncZendeskField({
|
||||
fieldId: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: langGeniusVersionInfo.current_env.toLowerCase(),
|
||||
previousValue: state.environment,
|
||||
setNextValue: (value) => {
|
||||
nextState.environment = value
|
||||
},
|
||||
}) || didSync
|
||||
didSync = syncZendeskField({
|
||||
fieldId: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: langGeniusVersionInfo.version,
|
||||
previousValue: state.version,
|
||||
setNextValue: (value) => {
|
||||
nextState.version = value
|
||||
},
|
||||
}) || didSync
|
||||
didSync = syncZendeskField({
|
||||
fieldId: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: userProfile.email,
|
||||
previousValue: state.email,
|
||||
setNextValue: (value) => {
|
||||
nextState.email = value
|
||||
},
|
||||
}) || didSync
|
||||
didSync = syncZendeskField({
|
||||
fieldId: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: currentWorkspace.id,
|
||||
previousValue: state.workspaceId,
|
||||
setNextValue: (value) => {
|
||||
nextState.workspaceId = value
|
||||
},
|
||||
}) || didSync
|
||||
|
||||
if (didSync)
|
||||
set(zendeskConversationSyncStateAtom, nextState)
|
||||
})
|
||||
@ -109,6 +109,7 @@
|
||||
"i18next-resources-to-backend": "catalog:",
|
||||
"immer": "catalog:",
|
||||
"jotai": "catalog:",
|
||||
"jotai-effect": "catalog:",
|
||||
"jotai-scope": "catalog:",
|
||||
"jotai-tanstack-form": "workspace:*",
|
||||
"jotai-tanstack-query": "catalog:",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user