mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
💄 style: implement data analytics event tracking framework (#8352)
This commit is contained in:
parent
13e1cafb62
commit
f433aca05f
19 changed files with 358 additions and 18 deletions
|
|
@ -144,6 +144,7 @@
|
|||
"@lobechat/electron-server-ipc": "workspace:*",
|
||||
"@lobechat/file-loaders": "workspace:*",
|
||||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/analytics": "^1.5.1",
|
||||
"@lobehub/charts": "^2.0.0",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Suspense } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
|
||||
import BrandTextLoading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
|
|
@ -31,6 +32,7 @@ const Layout = ({ children, topic, conversation, portal }: LayoutProps) => {
|
|||
</Portal>
|
||||
<TopicPanel>{topic}</TopicPanel>
|
||||
</Flexbox>
|
||||
<MainInterfaceTracker />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
|
|
@ -13,6 +14,7 @@ const Layout = ({ children, topic, conversation, portal }: LayoutProps) => {
|
|||
</MobileContentLayout>
|
||||
<TopicModal>{topic}</TopicModal>
|
||||
{portal}
|
||||
<MainInterfaceTracker />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useAnalytics } from '@lobehub/analytics/react';
|
||||
import { Empty } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -9,8 +10,10 @@ import LazyLoad from 'react-lazy-load';
|
|||
import { SESSION_CHAT_URL } from '@/const/url';
|
||||
import { useSwitchSession } from '@/hooks/useSwitchSession';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
import { getSessionStoreState, useSessionStore } from '@/store/session';
|
||||
import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
|
||||
import { getUserStoreState } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
import { LobeAgentSession } from '@/types/session';
|
||||
|
||||
import SkeletonList from '../../SkeletonList';
|
||||
|
|
@ -29,6 +32,7 @@ interface SessionListProps {
|
|||
}
|
||||
const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton = true }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { analytics } = useAnalytics();
|
||||
const { styles } = useStyles();
|
||||
|
||||
const isInit = useSessionStore(sessionSelectors.isSessionListInit);
|
||||
|
|
@ -49,6 +53,35 @@ const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
switchSession(id);
|
||||
|
||||
// Enhanced analytics tracking
|
||||
if (analytics) {
|
||||
const userStore = getUserStoreState();
|
||||
const sessionStore = getSessionStoreState();
|
||||
|
||||
const userId = userProfileSelectors.userId(userStore);
|
||||
const session = sessionSelectors.getSessionById(id)(sessionStore);
|
||||
|
||||
if (session) {
|
||||
const sessionGroupId = session.group || 'default';
|
||||
const group = sessionGroupSelectors.getGroupById(sessionGroupId)(sessionStore);
|
||||
const groupName =
|
||||
group?.name || (sessionGroupId === 'default' ? 'Default' : 'Unknown');
|
||||
|
||||
analytics?.track({
|
||||
name: 'switch_session',
|
||||
properties: {
|
||||
assistant_name: session.meta?.title || 'Untitled Agent',
|
||||
assistant_tags: session.meta?.tags || [],
|
||||
group_id: sessionGroupId,
|
||||
group_name: groupName,
|
||||
session_id: id,
|
||||
spm: 'homepage.chat.session_list_item.click',
|
||||
user_id: userId || 'anonymous',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SessionItem id={id} />
|
||||
|
|
|
|||
68
src/components/Analytics/LobeAnalyticsProvider.tsx
Normal file
68
src/components/Analytics/LobeAnalyticsProvider.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
'use client';
|
||||
|
||||
import { createSingletonAnalytics } from '@lobehub/analytics';
|
||||
import { AnalyticsProvider } from '@lobehub/analytics/react';
|
||||
import { ReactNode, memo, useMemo } from 'react';
|
||||
|
||||
import { BUSINESS_LINE } from '@/const/analytics';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { isDev } from '@/utils/env';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
debugPosthog: boolean;
|
||||
posthogEnabled: boolean;
|
||||
posthogHost: string;
|
||||
posthogToken: string;
|
||||
};
|
||||
|
||||
let analyticsInstance: ReturnType<typeof createSingletonAnalytics> | null = null;
|
||||
|
||||
export const LobeAnalyticsProvider = memo(
|
||||
({ children, posthogHost, posthogToken, posthogEnabled, debugPosthog }: Props) => {
|
||||
const analytics = useMemo(() => {
|
||||
if (analyticsInstance) {
|
||||
return analyticsInstance;
|
||||
}
|
||||
|
||||
analyticsInstance = createSingletonAnalytics({
|
||||
business: BUSINESS_LINE,
|
||||
debug: isDev,
|
||||
providers: {
|
||||
posthog: {
|
||||
debug: debugPosthog,
|
||||
enabled: posthogEnabled,
|
||||
host: posthogHost,
|
||||
key: posthogToken,
|
||||
person_profiles: 'always',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return analyticsInstance;
|
||||
}, []);
|
||||
|
||||
if (!analytics) return children;
|
||||
|
||||
return (
|
||||
<AnalyticsProvider
|
||||
client={analytics}
|
||||
onInitializeSuccess={() => {
|
||||
analyticsInstance?.setGlobalContext({
|
||||
platform: isDesktop ? 'desktop' : 'web',
|
||||
});
|
||||
|
||||
analyticsInstance
|
||||
?.getProvider('posthog')
|
||||
?.getNativeInstance()
|
||||
?.register({
|
||||
platform: isDesktop ? 'desktop' : 'web',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AnalyticsProvider>
|
||||
);
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
23
src/components/Analytics/LobeAnalyticsProviderWrapper.tsx
Normal file
23
src/components/Analytics/LobeAnalyticsProviderWrapper.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ReactNode, memo } from 'react';
|
||||
|
||||
import { LobeAnalyticsProvider } from '@/components/Analytics/LobeAnalyticsProvider';
|
||||
import { analyticsEnv } from '@/config/analytics';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const LobeAnalyticsProviderWrapper = memo<Props>(({ children }) => {
|
||||
return (
|
||||
<LobeAnalyticsProvider
|
||||
debugPosthog={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
|
||||
posthogEnabled={analyticsEnv.ENABLED_POSTHOG_ANALYTICS}
|
||||
posthogHost={analyticsEnv.POSTHOG_HOST}
|
||||
posthogToken={analyticsEnv.POSTHOG_KEY ?? ''}
|
||||
>
|
||||
{children}
|
||||
</LobeAnalyticsProvider>
|
||||
);
|
||||
});
|
||||
|
||||
LobeAnalyticsProviderWrapper.displayName = 'LobeAnalyticsProviderWrapper';
|
||||
52
src/components/Analytics/MainInterfaceTracker.tsx
Normal file
52
src/components/Analytics/MainInterfaceTracker.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { useAnalytics } from '@lobehub/analytics/react';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
|
||||
import { getChatStoreState } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/slices/message/selectors';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { getSessionStoreState } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
const MainInterfaceTracker = memo(() => {
|
||||
const { analytics } = useAnalytics();
|
||||
|
||||
const getMainInterfaceAnalyticsData = useCallback(() => {
|
||||
const currentSession = sessionSelectors.currentSession(getSessionStoreState());
|
||||
const activeSessionId = currentSession?.id;
|
||||
const defaultSessions = sessionSelectors.defaultSessions(getSessionStoreState());
|
||||
const showChatSideBar = systemStatusSelectors.showChatSideBar(useGlobalStore.getState());
|
||||
const messages = chatSelectors.activeBaseChats(getChatStoreState());
|
||||
return {
|
||||
active_assistant: activeSessionId === 'inbox' ? null : currentSession?.meta?.title || null,
|
||||
has_chat_history: messages.length > 0,
|
||||
session_id: activeSessionId ? activeSessionId : 'inbox',
|
||||
sidebar_state: showChatSideBar ? 'expanded' : 'collapsed',
|
||||
visible_assistants_count: defaultSessions.length,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!analytics) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
analytics.track({
|
||||
name: 'main_page_view',
|
||||
properties: {
|
||||
...getMainInterfaceAnalyticsData(),
|
||||
spm: 'main_page.interface.view',
|
||||
},
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [analytics, getMainInterfaceAnalyticsData]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
MainInterfaceTracker.displayName = 'MainInterfaceTracker';
|
||||
|
||||
export default MainInterfaceTracker;
|
||||
|
|
@ -8,7 +8,6 @@ import Google from './Google';
|
|||
import Vercel from './Vercel';
|
||||
|
||||
const Plausible = dynamic(() => import('./Plausible'));
|
||||
const Posthog = dynamic(() => import('./Posthog'));
|
||||
const Umami = dynamic(() => import('./Umami'));
|
||||
const Clarity = dynamic(() => import('./Clarity'));
|
||||
const ReactScan = dynamic(() => import('./ReactScan'));
|
||||
|
|
@ -24,13 +23,6 @@ const Analytics = () => {
|
|||
scriptBaseUrl={analyticsEnv.PLAUSIBLE_SCRIPT_BASE_URL}
|
||||
/>
|
||||
)}
|
||||
{analyticsEnv.ENABLED_POSTHOG_ANALYTICS && (
|
||||
<Posthog
|
||||
debug={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
|
||||
host={analyticsEnv.POSTHOG_HOST!}
|
||||
token={analyticsEnv.POSTHOG_KEY}
|
||||
/>
|
||||
)}
|
||||
{analyticsEnv.ENABLED_UMAMI_ANALYTICS && (
|
||||
<Umami
|
||||
scriptUrl={analyticsEnv.UMAMI_SCRIPT_URL}
|
||||
|
|
|
|||
3
src/const/analytics.ts
Normal file
3
src/const/analytics.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { isDesktop } from '@/const/version';
|
||||
|
||||
export const BUSINESS_LINE = isDesktop ? 'lobe-chat-desktop' : 'lobe-chat';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
||||
import { DeepPartial } from 'utility-types';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
|
|
@ -260,7 +261,31 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
|
|||
await get().dispatchConfig({ config, type: 'update' });
|
||||
},
|
||||
setAgentMeta: async (meta) => {
|
||||
await get().dispatchMeta({ type: 'update', value: meta });
|
||||
const { dispatchMeta, id, meta: currentMeta } = get();
|
||||
const mergedMeta = merge(currentMeta, meta);
|
||||
|
||||
try {
|
||||
const analytics = getSingletonAnalyticsOptional();
|
||||
if (analytics) {
|
||||
analytics.track({
|
||||
name: 'agent_meta_updated',
|
||||
properties: {
|
||||
assistant_avatar: mergedMeta.avatar,
|
||||
assistant_background_color: mergedMeta.backgroundColor,
|
||||
assistant_description: mergedMeta.description,
|
||||
assistant_name: mergedMeta.title,
|
||||
assistant_tags: mergedMeta.tags,
|
||||
is_inbox: id === 'inbox',
|
||||
session_id: id || 'unknown',
|
||||
timestamp: Date.now(),
|
||||
user_id: useUserStore.getState().user?.id || 'anonymous',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to track agent meta update:', error);
|
||||
}
|
||||
await dispatchMeta({ type: 'update', value: meta });
|
||||
},
|
||||
|
||||
setChatConfig: async (config) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { useAnalytics } from '@lobehub/analytics/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
|
||||
import { fileChatSelectors, useFileStore } from '@/store/file';
|
||||
import { getUserStoreState } from '@/store/user';
|
||||
import { SendMessageParams } from '@/types/message';
|
||||
|
||||
export type UseSendMessageParams = Pick<
|
||||
|
|
@ -15,6 +19,7 @@ export const useSendMessage = () => {
|
|||
s.sendMessage,
|
||||
s.updateInputMessage,
|
||||
]);
|
||||
const { analytics } = useAnalytics();
|
||||
|
||||
const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
|
||||
|
||||
|
|
@ -49,6 +54,29 @@ export const useSendMessage = () => {
|
|||
updateInputMessage('');
|
||||
clearChatUploadFileList();
|
||||
|
||||
// 获取分析数据
|
||||
const userStore = getUserStoreState();
|
||||
const agentStore = getAgentStoreState();
|
||||
|
||||
// 直接使用现有数据结构判断消息类型
|
||||
const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
|
||||
const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
|
||||
|
||||
analytics?.track({
|
||||
name: 'send_message',
|
||||
properties: {
|
||||
chat_id: store.activeId || 'unknown',
|
||||
current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
|
||||
has_attachments: fileList.length > 0,
|
||||
history_message_count: chatSelectors.activeBaseChats(store).length,
|
||||
message: store.inputMessage,
|
||||
message_length: store.inputMessage.length,
|
||||
message_type: messageType,
|
||||
selected_model: agentSelectors.currentAgentModel(agentStore),
|
||||
session_id: store.activeId || 'inbox', // 当前活跃的会话ID
|
||||
user_id: userStore.user?.id || 'anonymous',
|
||||
},
|
||||
});
|
||||
// const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
|
||||
// const agentSetting = useAgentStore.getState().agentSettingInstance;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useAnalytics } from '@lobehub/analytics/react';
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -7,12 +8,24 @@ import UserInfo from '../UserInfo';
|
|||
|
||||
const UserLoginOrSignup = memo<{ onClick: () => void }>(({ onClick }) => {
|
||||
const { t } = useTranslation('auth');
|
||||
const { analytics } = useAnalytics();
|
||||
|
||||
const handleClick = () => {
|
||||
analytics?.track({
|
||||
name: 'login_or_signup_clicked',
|
||||
properties: {
|
||||
spm: 'homepage.login_or_signup.click',
|
||||
},
|
||||
});
|
||||
|
||||
onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserInfo />
|
||||
<Flexbox paddingBlock={12} paddingInline={16} width={'100%'}>
|
||||
<Button block onClick={onClick} type={'primary'}>
|
||||
<Button block onClick={handleClick} type={'primary'}>
|
||||
{t('loginOrSignup')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactNode, Suspense } from 'react';
|
||||
|
||||
import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
|
||||
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
|
||||
import { appEnv } from '@/envs/app';
|
||||
import DevPanel from '@/features/DevPanel';
|
||||
|
|
@ -54,7 +55,9 @@ const GlobalLayout = async ({
|
|||
isMobile={isMobile}
|
||||
serverConfig={serverConfig}
|
||||
>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
<QueryProvider>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</QueryProvider>
|
||||
<StoreInitialization />
|
||||
<Suspense>
|
||||
<ImportSettings />
|
||||
|
|
|
|||
25
src/libs/analytics/index.ts
Normal file
25
src/libs/analytics/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { createServerAnalytics } from '@lobehub/analytics/server';
|
||||
|
||||
import { analyticsEnv } from '@/config/analytics';
|
||||
import { BUSINESS_LINE } from '@/const/analytics';
|
||||
import { isDev } from '@/utils/env';
|
||||
|
||||
export const serverAnalytics = createServerAnalytics({
|
||||
business: BUSINESS_LINE,
|
||||
debug: isDev,
|
||||
providers: {
|
||||
posthogNode: {
|
||||
debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
|
||||
enabled: analyticsEnv.ENABLED_POSTHOG_ANALYTICS,
|
||||
host: analyticsEnv.POSTHOG_HOST,
|
||||
key: analyticsEnv.POSTHOG_KEY ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const initializeServerAnalytics = async () => {
|
||||
await serverAnalytics.initialize();
|
||||
return serverAnalytics;
|
||||
};
|
||||
|
||||
export default serverAnalytics;
|
||||
|
|
@ -8,6 +8,14 @@ import { AgentService } from '@/server/services/agent';
|
|||
|
||||
import { UserService } from './index';
|
||||
|
||||
// Mock @/libs/analytics to avoid server-side environment variable access in client test environment
|
||||
vi.mock('@/libs/analytics', () => ({
|
||||
initializeServerAnalytics: vi.fn().mockResolvedValue({
|
||||
identify: vi.fn(),
|
||||
track: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/database/models/user', () => {
|
||||
const MockUserModel = vi.fn();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { UserJSON } from '@clerk/backend';
|
|||
|
||||
import { UserModel } from '@/database/models/user';
|
||||
import { serverDB } from '@/database/server';
|
||||
import { initializeServerAnalytics } from '@/libs/analytics';
|
||||
import { pino } from '@/libs/logger';
|
||||
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
||||
import { S3 } from '@/server/modules/S3';
|
||||
|
|
@ -51,6 +52,23 @@ export class UserService {
|
|||
|
||||
/* ↑ cloud slot ↑ */
|
||||
|
||||
//analytics
|
||||
const analytics = await initializeServerAnalytics();
|
||||
analytics?.identify(id, {
|
||||
email: email?.email_address,
|
||||
firstName: params.first_name,
|
||||
lastName: params.last_name,
|
||||
phone: phone?.phone_number,
|
||||
username: params.username,
|
||||
});
|
||||
analytics?.track({
|
||||
name: 'user_register_completed',
|
||||
properties: {
|
||||
spm: 'user_service.create_user.user_created',
|
||||
},
|
||||
userId: id,
|
||||
});
|
||||
|
||||
return { message: 'user created', success: true };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { t } from 'i18next';
|
||||
import useSWR, { SWRResponse, mutate } from 'swr';
|
||||
|
|
@ -10,8 +11,8 @@ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
|
|||
import { useClientDataSWR } from '@/libs/swr';
|
||||
import { sessionService } from '@/services/session';
|
||||
import { SessionStore } from '@/store/session';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { getUserStoreState, useUserStore } from '@/store/user';
|
||||
import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';
|
||||
import { MetaData } from '@/types/meta';
|
||||
import {
|
||||
ChatSessionList,
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
import { merge } from '@/utils/merge';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { sessionGroupSelectors } from '../sessionGroup/selectors';
|
||||
import { SessionDispatch, sessionsReducer } from './reducers';
|
||||
import { sessionSelectors } from './selectors';
|
||||
import { sessionMetaSelectors } from './selectors/meta';
|
||||
|
|
@ -114,6 +116,30 @@ export const createSessionSlice: StateCreator<
|
|||
const id = await sessionService.createSession(LobeSessionType.Agent, newSession);
|
||||
await refreshSessions();
|
||||
|
||||
// Track new agent creation analytics
|
||||
const analytics = getSingletonAnalyticsOptional();
|
||||
if (analytics) {
|
||||
const userStore = getUserStoreState();
|
||||
const userId = userProfileSelectors.userId(userStore);
|
||||
|
||||
// Get group information
|
||||
const groupId = newSession.group || 'default';
|
||||
const group = sessionGroupSelectors.getGroupById(groupId)(get());
|
||||
const groupName = group?.name || (groupId === 'default' ? 'Default' : 'Unknown');
|
||||
|
||||
analytics.track({
|
||||
name: 'new_agent_created',
|
||||
properties: {
|
||||
assistant_name: newSession.meta?.title || 'Untitled Agent',
|
||||
assistant_tags: newSession.meta?.tags || [],
|
||||
group_id: groupId,
|
||||
group_name: groupName,
|
||||
session_id: id,
|
||||
user_id: userId || 'anonymous',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Whether to goto to the new session after creation, the default is to switch to
|
||||
if (isSwitchSession) switchSession(id);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
||||
import useSWR, { SWRResponse, mutate } from 'swr';
|
||||
import { DeepPartial } from 'utility-types';
|
||||
import type { StateCreator } from 'zustand/vanilla';
|
||||
|
|
@ -22,7 +23,6 @@ const GET_USER_STATE_KEY = 'initUserState';
|
|||
*/
|
||||
export interface CommonAction {
|
||||
refreshUserState: () => Promise<void>;
|
||||
|
||||
updateAvatar: (avatar: string) => Promise<void>;
|
||||
useCheckTrace: (shouldFetch: boolean) => SWRResponse;
|
||||
useInitUserState: (
|
||||
|
|
@ -118,7 +118,14 @@ export const createCommonSlice: StateCreator<
|
|||
false,
|
||||
n('initUserState'),
|
||||
);
|
||||
|
||||
//analytics
|
||||
const analytics = getSingletonAnalyticsOptional();
|
||||
analytics?.identify(data.userId || '', {
|
||||
email: data.email,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
username: data.username,
|
||||
});
|
||||
get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ import { theme } from 'antd';
|
|||
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
|
||||
import 'fake-indexeddb/auto';
|
||||
import React from 'react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Global mock for @lobehub/analytics/react to avoid AnalyticsProvider dependency
|
||||
// This prevents tests from failing when components use useAnalytics hook
|
||||
vi.mock('@lobehub/analytics/react', () => ({
|
||||
useAnalytics: () => ({
|
||||
analytics: {
|
||||
track: vi.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// only inject in the dom environment
|
||||
if (
|
||||
|
|
|
|||
Loading…
Reference in a new issue