@lobehub/chat 1.97.1 → 1.97.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/changelog/v1.json +14 -0
  3. package/docker-compose/local/docker-compose.yml +1 -1
  4. package/docker-compose/local/logto/docker-compose.yml +1 -1
  5. package/docker-compose/local/zitadel/docker-compose.yml +1 -1
  6. package/docker-compose/production/logto/docker-compose.yml +1 -1
  7. package/docker-compose/production/zitadel/docker-compose.yml +1 -1
  8. package/docs/self-hosting/server-database/docker-compose.mdx +1 -1
  9. package/package.json +2 -1
  10. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/index.tsx +2 -0
  11. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Mobile/index.tsx +2 -0
  12. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/index.tsx +35 -2
  13. package/src/components/Analytics/LobeAnalyticsProvider.tsx +68 -0
  14. package/src/components/Analytics/LobeAnalyticsProviderWrapper.tsx +23 -0
  15. package/src/components/Analytics/MainInterfaceTracker.tsx +52 -0
  16. package/src/components/Analytics/index.tsx +0 -8
  17. package/src/const/analytics.ts +3 -0
  18. package/src/features/AgentSetting/store/action.ts +26 -1
  19. package/src/features/ChatInput/useSend.ts +29 -1
  20. package/src/features/User/UserLoginOrSignup/Community.tsx +14 -1
  21. package/src/layout/GlobalProvider/index.tsx +4 -1
  22. package/src/libs/analytics/index.ts +25 -0
  23. package/src/server/services/user/index.test.ts +8 -0
  24. package/src/server/services/user/index.ts +18 -0
  25. package/src/store/session/slices/session/action.ts +28 -2
  26. package/src/store/user/slices/common/action.ts +9 -2
  27. package/tests/setup.ts +11 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.97.3](https://github.com/lobehub/lobe-chat/compare/v1.97.2...v1.97.3)
6
+
7
+ <sup>Released on **2025-07-10**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 1.97.2](https://github.com/lobehub/lobe-chat/compare/v1.97.1...v1.97.2)
23
+
24
+ <sup>Released on **2025-07-10**</sup>
25
+
26
+ #### 💄 Styles
27
+
28
+ - **misc**: Implement data analytics event tracking framework.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Styles
36
+
37
+ - **misc**: Implement data analytics event tracking framework, closes [#8352](https://github.com/lobehub/lobe-chat/issues/8352) ([f433aca](https://github.com/lobehub/lobe-chat/commit/f433aca))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 1.97.1](https://github.com/lobehub/lobe-chat/compare/v1.97.0...v1.97.1)
6
48
 
7
49
  <sup>Released on **2025-07-10**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2025-07-10",
5
+ "version": "1.97.3"
6
+ },
7
+ {
8
+ "children": {
9
+ "improvements": [
10
+ "Implement data analytics event tracking framework."
11
+ ]
12
+ },
13
+ "date": "2025-07-10",
14
+ "version": "1.97.2"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "fixes": [
@@ -33,7 +33,7 @@ services:
33
33
  - lobe-network
34
34
 
35
35
  minio:
36
- image: minio/minio
36
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
37
37
  container_name: lobe-minio
38
38
  network_mode: 'service:network-service'
39
39
  volumes:
@@ -33,7 +33,7 @@ services:
33
33
  - lobe-network
34
34
 
35
35
  minio:
36
- image: minio/minio
36
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
37
37
  container_name: lobe-minio
38
38
  network_mode: 'service:network-service'
39
39
  volumes:
@@ -32,7 +32,7 @@ services:
32
32
  - lobe-network
33
33
 
34
34
  minio:
35
- image: minio/minio
35
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
36
36
  container_name: lobe-minio
37
37
  network_mode: 'service:network-service'
38
38
  volumes:
@@ -18,7 +18,7 @@ services:
18
18
  restart: always
19
19
 
20
20
  minio:
21
- image: minio/minio
21
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
22
22
  container_name: lobe-minio
23
23
  ports:
24
24
  - '9000:9000'
@@ -18,7 +18,7 @@ services:
18
18
  restart: always
19
19
 
20
20
  minio:
21
- image: minio/minio
21
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
22
22
  container_name: lobe-minio
23
23
  ports:
24
24
  - '9000:9000'
@@ -754,7 +754,7 @@ services:
754
754
  - lobe-network
755
755
 
756
756
  minio:
757
- image: minio/minio
757
+ image: minio/minio:RELEASE.2025-04-22T22-12-26Z
758
758
  container_name: lobe-minio
759
759
  network_mode: 'service:network-service'
760
760
  volumes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.97.1",
3
+ "version": "1.97.3",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -144,6 +144,7 @@
144
144
  "@lobechat/electron-server-ipc": "workspace:*",
145
145
  "@lobechat/file-loaders": "workspace:*",
146
146
  "@lobechat/web-crawler": "workspace:*",
147
+ "@lobehub/analytics": "^1.5.1",
147
148
  "@lobehub/charts": "^2.0.0",
148
149
  "@lobehub/chat-plugin-sdk": "^1.32.4",
149
150
  "@lobehub/chat-plugins-gateway": "^1.9.0",
@@ -1,6 +1,7 @@
1
1
  import { Suspense } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
+ import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
4
5
  import BrandTextLoading from '@/components/Loading/BrandTextLoading';
5
6
 
6
7
  import { LayoutProps } from '../type';
@@ -31,6 +32,7 @@ const Layout = ({ children, topic, conversation, portal }: LayoutProps) => {
31
32
  </Portal>
32
33
  <TopicPanel>{topic}</TopicPanel>
33
34
  </Flexbox>
35
+ <MainInterfaceTracker />
34
36
  </>
35
37
  );
36
38
  };
@@ -1,3 +1,4 @@
1
+ import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
1
2
  import MobileContentLayout from '@/components/server/MobileNavLayout';
2
3
 
3
4
  import { LayoutProps } from '../type';
@@ -13,6 +14,7 @@ const Layout = ({ children, topic, conversation, portal }: LayoutProps) => {
13
14
  </MobileContentLayout>
14
15
  <TopicModal>{topic}</TopicModal>
15
16
  {portal}
17
+ <MainInterfaceTracker />
16
18
  </>
17
19
  );
18
20
  };
@@ -1,3 +1,4 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
1
2
  import { Empty } from 'antd';
2
3
  import { createStyles } from 'antd-style';
3
4
  import Link from 'next/link';
@@ -9,8 +10,10 @@ import LazyLoad from 'react-lazy-load';
9
10
  import { SESSION_CHAT_URL } from '@/const/url';
10
11
  import { useSwitchSession } from '@/hooks/useSwitchSession';
11
12
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
12
- import { useSessionStore } from '@/store/session';
13
- import { sessionSelectors } from '@/store/session/selectors';
13
+ import { getSessionStoreState, useSessionStore } from '@/store/session';
14
+ import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
15
+ import { getUserStoreState } from '@/store/user';
16
+ import { userProfileSelectors } from '@/store/user/selectors';
14
17
  import { LobeAgentSession } from '@/types/session';
15
18
 
16
19
  import SkeletonList from '../../SkeletonList';
@@ -29,6 +32,7 @@ interface SessionListProps {
29
32
  }
30
33
  const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton = true }) => {
31
34
  const { t } = useTranslation('chat');
35
+ const { analytics } = useAnalytics();
32
36
  const { styles } = useStyles();
33
37
 
34
38
  const isInit = useSessionStore(sessionSelectors.isSessionListInit);
@@ -49,6 +53,35 @@ const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton
49
53
  onClick={(e) => {
50
54
  e.preventDefault();
51
55
  switchSession(id);
56
+
57
+ // Enhanced analytics tracking
58
+ if (analytics) {
59
+ const userStore = getUserStoreState();
60
+ const sessionStore = getSessionStoreState();
61
+
62
+ const userId = userProfileSelectors.userId(userStore);
63
+ const session = sessionSelectors.getSessionById(id)(sessionStore);
64
+
65
+ if (session) {
66
+ const sessionGroupId = session.group || 'default';
67
+ const group = sessionGroupSelectors.getGroupById(sessionGroupId)(sessionStore);
68
+ const groupName =
69
+ group?.name || (sessionGroupId === 'default' ? 'Default' : 'Unknown');
70
+
71
+ analytics?.track({
72
+ name: 'switch_session',
73
+ properties: {
74
+ assistant_name: session.meta?.title || 'Untitled Agent',
75
+ assistant_tags: session.meta?.tags || [],
76
+ group_id: sessionGroupId,
77
+ group_name: groupName,
78
+ session_id: id,
79
+ spm: 'homepage.chat.session_list_item.click',
80
+ user_id: userId || 'anonymous',
81
+ },
82
+ });
83
+ }
84
+ }
52
85
  }}
53
86
  >
54
87
  <SessionItem id={id} />
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { createSingletonAnalytics } from '@lobehub/analytics';
4
+ import { AnalyticsProvider } from '@lobehub/analytics/react';
5
+ import { ReactNode, memo, useMemo } from 'react';
6
+
7
+ import { BUSINESS_LINE } from '@/const/analytics';
8
+ import { isDesktop } from '@/const/version';
9
+ import { isDev } from '@/utils/env';
10
+
11
+ type Props = {
12
+ children: ReactNode;
13
+ debugPosthog: boolean;
14
+ posthogEnabled: boolean;
15
+ posthogHost: string;
16
+ posthogToken: string;
17
+ };
18
+
19
+ let analyticsInstance: ReturnType<typeof createSingletonAnalytics> | null = null;
20
+
21
+ export const LobeAnalyticsProvider = memo(
22
+ ({ children, posthogHost, posthogToken, posthogEnabled, debugPosthog }: Props) => {
23
+ const analytics = useMemo(() => {
24
+ if (analyticsInstance) {
25
+ return analyticsInstance;
26
+ }
27
+
28
+ analyticsInstance = createSingletonAnalytics({
29
+ business: BUSINESS_LINE,
30
+ debug: isDev,
31
+ providers: {
32
+ posthog: {
33
+ debug: debugPosthog,
34
+ enabled: posthogEnabled,
35
+ host: posthogHost,
36
+ key: posthogToken,
37
+ person_profiles: 'always',
38
+ },
39
+ },
40
+ });
41
+
42
+ return analyticsInstance;
43
+ }, []);
44
+
45
+ if (!analytics) return children;
46
+
47
+ return (
48
+ <AnalyticsProvider
49
+ client={analytics}
50
+ onInitializeSuccess={() => {
51
+ analyticsInstance?.setGlobalContext({
52
+ platform: isDesktop ? 'desktop' : 'web',
53
+ });
54
+
55
+ analyticsInstance
56
+ ?.getProvider('posthog')
57
+ ?.getNativeInstance()
58
+ ?.register({
59
+ platform: isDesktop ? 'desktop' : 'web',
60
+ });
61
+ }}
62
+ >
63
+ {children}
64
+ </AnalyticsProvider>
65
+ );
66
+ },
67
+ () => true,
68
+ );
@@ -0,0 +1,23 @@
1
+ import { ReactNode, memo } from 'react';
2
+
3
+ import { LobeAnalyticsProvider } from '@/components/Analytics/LobeAnalyticsProvider';
4
+ import { analyticsEnv } from '@/config/analytics';
5
+
6
+ type Props = {
7
+ children: ReactNode;
8
+ };
9
+
10
+ export const LobeAnalyticsProviderWrapper = memo<Props>(({ children }) => {
11
+ return (
12
+ <LobeAnalyticsProvider
13
+ debugPosthog={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
14
+ posthogEnabled={analyticsEnv.ENABLED_POSTHOG_ANALYTICS}
15
+ posthogHost={analyticsEnv.POSTHOG_HOST}
16
+ posthogToken={analyticsEnv.POSTHOG_KEY ?? ''}
17
+ >
18
+ {children}
19
+ </LobeAnalyticsProvider>
20
+ );
21
+ });
22
+
23
+ LobeAnalyticsProviderWrapper.displayName = 'LobeAnalyticsProviderWrapper';
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { useAnalytics } from '@lobehub/analytics/react';
4
+ import { memo, useCallback, useEffect } from 'react';
5
+
6
+ import { getChatStoreState } from '@/store/chat';
7
+ import { chatSelectors } from '@/store/chat/slices/message/selectors';
8
+ import { useGlobalStore } from '@/store/global';
9
+ import { systemStatusSelectors } from '@/store/global/selectors';
10
+ import { getSessionStoreState } from '@/store/session';
11
+ import { sessionSelectors } from '@/store/session/selectors';
12
+
13
+ const MainInterfaceTracker = memo(() => {
14
+ const { analytics } = useAnalytics();
15
+
16
+ const getMainInterfaceAnalyticsData = useCallback(() => {
17
+ const currentSession = sessionSelectors.currentSession(getSessionStoreState());
18
+ const activeSessionId = currentSession?.id;
19
+ const defaultSessions = sessionSelectors.defaultSessions(getSessionStoreState());
20
+ const showChatSideBar = systemStatusSelectors.showChatSideBar(useGlobalStore.getState());
21
+ const messages = chatSelectors.activeBaseChats(getChatStoreState());
22
+ return {
23
+ active_assistant: activeSessionId === 'inbox' ? null : currentSession?.meta?.title || null,
24
+ has_chat_history: messages.length > 0,
25
+ session_id: activeSessionId ? activeSessionId : 'inbox',
26
+ sidebar_state: showChatSideBar ? 'expanded' : 'collapsed',
27
+ visible_assistants_count: defaultSessions.length,
28
+ };
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ if (!analytics) return;
33
+
34
+ const timer = setTimeout(() => {
35
+ analytics.track({
36
+ name: 'main_page_view',
37
+ properties: {
38
+ ...getMainInterfaceAnalyticsData(),
39
+ spm: 'main_page.interface.view',
40
+ },
41
+ });
42
+ }, 1000);
43
+
44
+ return () => clearTimeout(timer);
45
+ }, [analytics, getMainInterfaceAnalyticsData]);
46
+
47
+ return null;
48
+ });
49
+
50
+ MainInterfaceTracker.displayName = 'MainInterfaceTracker';
51
+
52
+ export default MainInterfaceTracker;
@@ -8,7 +8,6 @@ import Google from './Google';
8
8
  import Vercel from './Vercel';
9
9
 
10
10
  const Plausible = dynamic(() => import('./Plausible'));
11
- const Posthog = dynamic(() => import('./Posthog'));
12
11
  const Umami = dynamic(() => import('./Umami'));
13
12
  const Clarity = dynamic(() => import('./Clarity'));
14
13
  const ReactScan = dynamic(() => import('./ReactScan'));
@@ -24,13 +23,6 @@ const Analytics = () => {
24
23
  scriptBaseUrl={analyticsEnv.PLAUSIBLE_SCRIPT_BASE_URL}
25
24
  />
26
25
  )}
27
- {analyticsEnv.ENABLED_POSTHOG_ANALYTICS && (
28
- <Posthog
29
- debug={analyticsEnv.DEBUG_POSTHOG_ANALYTICS}
30
- host={analyticsEnv.POSTHOG_HOST!}
31
- token={analyticsEnv.POSTHOG_KEY}
32
- />
33
- )}
34
26
  {analyticsEnv.ENABLED_UMAMI_ANALYTICS && (
35
27
  <Umami
36
28
  scriptUrl={analyticsEnv.UMAMI_SCRIPT_URL}
@@ -0,0 +1,3 @@
1
+ import { isDesktop } from '@/const/version';
2
+
3
+ export const BUSINESS_LINE = isDesktop ? 'lobe-chat-desktop' : 'lobe-chat';
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import { DeepPartial } from 'utility-types';
2
3
  import { StateCreator } from 'zustand/vanilla';
3
4
 
@@ -260,7 +261,31 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
260
261
  await get().dispatchConfig({ config, type: 'update' });
261
262
  },
262
263
  setAgentMeta: async (meta) => {
263
- await get().dispatchMeta({ type: 'update', value: meta });
264
+ const { dispatchMeta, id, meta: currentMeta } = get();
265
+ const mergedMeta = merge(currentMeta, meta);
266
+
267
+ try {
268
+ const analytics = getSingletonAnalyticsOptional();
269
+ if (analytics) {
270
+ analytics.track({
271
+ name: 'agent_meta_updated',
272
+ properties: {
273
+ assistant_avatar: mergedMeta.avatar,
274
+ assistant_background_color: mergedMeta.backgroundColor,
275
+ assistant_description: mergedMeta.description,
276
+ assistant_name: mergedMeta.title,
277
+ assistant_tags: mergedMeta.tags,
278
+ is_inbox: id === 'inbox',
279
+ session_id: id || 'unknown',
280
+ timestamp: Date.now(),
281
+ user_id: useUserStore.getState().user?.id || 'anonymous',
282
+ },
283
+ });
284
+ }
285
+ } catch (error) {
286
+ console.warn('Failed to track agent meta update:', error);
287
+ }
288
+ await dispatchMeta({ type: 'update', value: meta });
264
289
  },
265
290
 
266
291
  setChatConfig: async (config) => {
@@ -1,8 +1,12 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
1
2
  import { useCallback, useMemo } from 'react';
2
3
 
4
+ import { getAgentStoreState } from '@/store/agent';
5
+ import { agentSelectors } from '@/store/agent/selectors';
3
6
  import { useChatStore } from '@/store/chat';
4
- import { chatSelectors } from '@/store/chat/selectors';
7
+ import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
5
8
  import { fileChatSelectors, useFileStore } from '@/store/file';
9
+ import { getUserStoreState } from '@/store/user';
6
10
  import { SendMessageParams } from '@/types/message';
7
11
 
8
12
  export type UseSendMessageParams = Pick<
@@ -15,6 +19,7 @@ export const useSendMessage = () => {
15
19
  s.sendMessage,
16
20
  s.updateInputMessage,
17
21
  ]);
22
+ const { analytics } = useAnalytics();
18
23
 
19
24
  const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
20
25
 
@@ -49,6 +54,29 @@ export const useSendMessage = () => {
49
54
  updateInputMessage('');
50
55
  clearChatUploadFileList();
51
56
 
57
+ // 获取分析数据
58
+ const userStore = getUserStoreState();
59
+ const agentStore = getAgentStoreState();
60
+
61
+ // 直接使用现有数据结构判断消息类型
62
+ const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
63
+ const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
64
+
65
+ analytics?.track({
66
+ name: 'send_message',
67
+ properties: {
68
+ chat_id: store.activeId || 'unknown',
69
+ current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
70
+ has_attachments: fileList.length > 0,
71
+ history_message_count: chatSelectors.activeBaseChats(store).length,
72
+ message: store.inputMessage,
73
+ message_length: store.inputMessage.length,
74
+ message_type: messageType,
75
+ selected_model: agentSelectors.currentAgentModel(agentStore),
76
+ session_id: store.activeId || 'inbox', // 当前活跃的会话ID
77
+ user_id: userStore.user?.id || 'anonymous',
78
+ },
79
+ });
52
80
  // const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
53
81
  // const agentSetting = useAgentStore.getState().agentSettingInstance;
54
82
 
@@ -1,3 +1,4 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
1
2
  import { Button } from '@lobehub/ui';
2
3
  import { memo } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
@@ -7,12 +8,24 @@ import UserInfo from '../UserInfo';
7
8
 
8
9
  const UserLoginOrSignup = memo<{ onClick: () => void }>(({ onClick }) => {
9
10
  const { t } = useTranslation('auth');
11
+ const { analytics } = useAnalytics();
12
+
13
+ const handleClick = () => {
14
+ analytics?.track({
15
+ name: 'login_or_signup_clicked',
16
+ properties: {
17
+ spm: 'homepage.login_or_signup.click',
18
+ },
19
+ });
20
+
21
+ onClick();
22
+ };
10
23
 
11
24
  return (
12
25
  <>
13
26
  <UserInfo />
14
27
  <Flexbox paddingBlock={12} paddingInline={16} width={'100%'}>
15
- <Button block onClick={onClick} type={'primary'}>
28
+ <Button block onClick={handleClick} type={'primary'}>
16
29
  {t('loginOrSignup')}
17
30
  </Button>
18
31
  </Flexbox>
@@ -1,5 +1,6 @@
1
1
  import { ReactNode, Suspense } from 'react';
2
2
 
3
+ import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
3
4
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
4
5
  import { appEnv } from '@/envs/app';
5
6
  import DevPanel from '@/features/DevPanel';
@@ -54,7 +55,9 @@ const GlobalLayout = async ({
54
55
  isMobile={isMobile}
55
56
  serverConfig={serverConfig}
56
57
  >
57
- <QueryProvider>{children}</QueryProvider>
58
+ <QueryProvider>
59
+ <LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
60
+ </QueryProvider>
58
61
  <StoreInitialization />
59
62
  <Suspense>
60
63
  <ImportSettings />
@@ -0,0 +1,25 @@
1
+ import { createServerAnalytics } from '@lobehub/analytics/server';
2
+
3
+ import { analyticsEnv } from '@/config/analytics';
4
+ import { BUSINESS_LINE } from '@/const/analytics';
5
+ import { isDev } from '@/utils/env';
6
+
7
+ export const serverAnalytics = createServerAnalytics({
8
+ business: BUSINESS_LINE,
9
+ debug: isDev,
10
+ providers: {
11
+ posthogNode: {
12
+ debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
13
+ enabled: analyticsEnv.ENABLED_POSTHOG_ANALYTICS,
14
+ host: analyticsEnv.POSTHOG_HOST,
15
+ key: analyticsEnv.POSTHOG_KEY ?? '',
16
+ },
17
+ },
18
+ });
19
+
20
+ export const initializeServerAnalytics = async () => {
21
+ await serverAnalytics.initialize();
22
+ return serverAnalytics;
23
+ };
24
+
25
+ export default serverAnalytics;
@@ -8,6 +8,14 @@ import { AgentService } from '@/server/services/agent';
8
8
 
9
9
  import { UserService } from './index';
10
10
 
11
+ // Mock @/libs/analytics to avoid server-side environment variable access in client test environment
12
+ vi.mock('@/libs/analytics', () => ({
13
+ initializeServerAnalytics: vi.fn().mockResolvedValue({
14
+ identify: vi.fn(),
15
+ track: vi.fn(),
16
+ }),
17
+ }));
18
+
11
19
  // Mock dependencies
12
20
  vi.mock('@/database/models/user', () => {
13
21
  const MockUserModel = vi.fn();
@@ -2,6 +2,7 @@ import { UserJSON } from '@clerk/backend';
2
2
 
3
3
  import { UserModel } from '@/database/models/user';
4
4
  import { serverDB } from '@/database/server';
5
+ import { initializeServerAnalytics } from '@/libs/analytics';
5
6
  import { pino } from '@/libs/logger';
6
7
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
8
  import { S3 } from '@/server/modules/S3';
@@ -51,6 +52,23 @@ export class UserService {
51
52
 
52
53
  /* ↑ cloud slot ↑ */
53
54
 
55
+ //analytics
56
+ const analytics = await initializeServerAnalytics();
57
+ analytics?.identify(id, {
58
+ email: email?.email_address,
59
+ firstName: params.first_name,
60
+ lastName: params.last_name,
61
+ phone: phone?.phone_number,
62
+ username: params.username,
63
+ });
64
+ analytics?.track({
65
+ name: 'user_register_completed',
66
+ properties: {
67
+ spm: 'user_service.create_user.user_created',
68
+ },
69
+ userId: id,
70
+ });
71
+
54
72
  return { message: 'user created', success: true };
55
73
  };
56
74
 
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import isEqual from 'fast-deep-equal';
2
3
  import { t } from 'i18next';
3
4
  import useSWR, { SWRResponse, mutate } from 'swr';
@@ -10,8 +11,8 @@ import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
10
11
  import { useClientDataSWR } from '@/libs/swr';
11
12
  import { sessionService } from '@/services/session';
12
13
  import { SessionStore } from '@/store/session';
13
- import { useUserStore } from '@/store/user';
14
- import { settingsSelectors } from '@/store/user/selectors';
14
+ import { getUserStoreState, useUserStore } from '@/store/user';
15
+ import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';
15
16
  import { MetaData } from '@/types/meta';
16
17
  import {
17
18
  ChatSessionList,
@@ -24,6 +25,7 @@ import {
24
25
  import { merge } from '@/utils/merge';
25
26
  import { setNamespace } from '@/utils/storeDebug';
26
27
 
28
+ import { sessionGroupSelectors } from '../sessionGroup/selectors';
27
29
  import { SessionDispatch, sessionsReducer } from './reducers';
28
30
  import { sessionSelectors } from './selectors';
29
31
  import { sessionMetaSelectors } from './selectors/meta';
@@ -114,6 +116,30 @@ export const createSessionSlice: StateCreator<
114
116
  const id = await sessionService.createSession(LobeSessionType.Agent, newSession);
115
117
  await refreshSessions();
116
118
 
119
+ // Track new agent creation analytics
120
+ const analytics = getSingletonAnalyticsOptional();
121
+ if (analytics) {
122
+ const userStore = getUserStoreState();
123
+ const userId = userProfileSelectors.userId(userStore);
124
+
125
+ // Get group information
126
+ const groupId = newSession.group || 'default';
127
+ const group = sessionGroupSelectors.getGroupById(groupId)(get());
128
+ const groupName = group?.name || (groupId === 'default' ? 'Default' : 'Unknown');
129
+
130
+ analytics.track({
131
+ name: 'new_agent_created',
132
+ properties: {
133
+ assistant_name: newSession.meta?.title || 'Untitled Agent',
134
+ assistant_tags: newSession.meta?.tags || [],
135
+ group_id: groupId,
136
+ group_name: groupName,
137
+ session_id: id,
138
+ user_id: userId || 'anonymous',
139
+ },
140
+ });
141
+ }
142
+
117
143
  // Whether to goto to the new session after creation, the default is to switch to
118
144
  if (isSwitchSession) switchSession(id);
119
145
 
@@ -1,3 +1,4 @@
1
+ import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
1
2
  import useSWR, { SWRResponse, mutate } from 'swr';
2
3
  import { DeepPartial } from 'utility-types';
3
4
  import type { StateCreator } from 'zustand/vanilla';
@@ -22,7 +23,6 @@ const GET_USER_STATE_KEY = 'initUserState';
22
23
  */
23
24
  export interface CommonAction {
24
25
  refreshUserState: () => Promise<void>;
25
-
26
26
  updateAvatar: (avatar: string) => Promise<void>;
27
27
  useCheckTrace: (shouldFetch: boolean) => SWRResponse;
28
28
  useInitUserState: (
@@ -118,7 +118,14 @@ export const createCommonSlice: StateCreator<
118
118
  false,
119
119
  n('initUserState'),
120
120
  );
121
-
121
+ //analytics
122
+ const analytics = getSingletonAnalyticsOptional();
123
+ analytics?.identify(data.userId || '', {
124
+ email: data.email,
125
+ firstName: data.firstName,
126
+ lastName: data.lastName,
127
+ username: data.username,
128
+ });
122
129
  get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
123
130
  }
124
131
  },
package/tests/setup.ts CHANGED
@@ -5,6 +5,17 @@ import { theme } from 'antd';
5
5
  // refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
6
6
  import 'fake-indexeddb/auto';
7
7
  import React from 'react';
8
+ import { vi } from 'vitest';
9
+
10
+ // Global mock for @lobehub/analytics/react to avoid AnalyticsProvider dependency
11
+ // This prevents tests from failing when components use useAnalytics hook
12
+ vi.mock('@lobehub/analytics/react', () => ({
13
+ useAnalytics: () => ({
14
+ analytics: {
15
+ track: vi.fn(),
16
+ },
17
+ }),
18
+ }));
8
19
 
9
20
  // only inject in the dom environment
10
21
  if (