From 52428df1bd2fb09d4d61577923dbd7f2401518c8 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 27 Jul 2026 13:54:45 +0800 Subject: [PATCH] feat: add amptitude to new agent (#39608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 林玮 (Jade Lin) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../__tests__/chat-wrapper.spec.tsx | 39 ++++++++++++++++++- .../chat/chat-with-history/chat-wrapper.tsx | 5 +++ .../__tests__/chat-wrapper.spec.tsx | 11 ++++++ .../chat/embedded-chatbot/chat-wrapper.tsx | 4 ++ .../hooks/__tests__/use-result-sender.spec.ts | 27 +++++++++++++ .../result/hooks/use-result-sender.ts | 7 ++++ .../configure/__tests__/page.spec.tsx | 11 +++++- .../use-agent-configure-sync.spec.tsx | 16 ++++++++ .../configure/components/composer-session.tsx | 6 ++- .../configure/use-agent-configure-sync.ts | 10 +++++ .../__tests__/create-agent-dialog.spec.tsx | 10 +++++ .../roster/components/create-agent-dialog.tsx | 5 +++ web/models/share.ts | 2 + .../__tests__/create-app-tracking.spec.ts | 17 ++++++++ web/utils/create-app-tracking.ts | 4 +- 15 files changed, 170 insertions(+), 4 deletions(-) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx index 01390834a7f..680915c7138 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx @@ -17,6 +17,12 @@ import { isValidGeneratedAnswer } from '../../utils' import ChatWrapper from '../chat-wrapper' import { useChatWithHistoryContext } from '../context' +const mockTrackEvent = vi.hoisted(() => vi.fn()) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: mockTrackEvent, +})) + vi.mock('../../chat/hooks', () => ({ useChat: vi.fn(), })) @@ -75,6 +81,7 @@ vi.mock('@/hooks/use-timestamp', () => ({ type ChatHookReturn = ReturnType const mockAppData = { + mode: 'advanced-chat', site: { title: 'Test Chat', chat_color_theme: 'blue', @@ -745,7 +752,34 @@ describe('ChatWrapper', () => { expect(fetchChatList).toHaveBeenCalledWith('conversation-1', 'webApp', 'test-app-id') }) - it('should not fetch current conversation messages for non-new-agent chat', async () => { + it('should track the start action when a new agent web app sends a message', async () => { + const handleSend = vi.fn() + vi.mocked(useChat).mockReturnValue({ + ...defaultChatHookReturn, + handleSend, + chatList: [ + { id: '1', isOpeningStatement: true, content: 'Welcome', suggestedQuestions: ['Q1'] }, + ], + suggestedQuestions: ['Q1'], + } as unknown as ChatHookReturn) + vi.mocked(useChatWithHistoryContext).mockReturnValue({ + ...defaultContextValue, + currentConversationId: '', + isInstalledApp: false, + isNewAgent: true, + }) + + render() + + fireEvent.click(await screen.findByText('Q1')) + + expect(handleSend).toHaveBeenCalled() + expect(mockTrackEvent).toHaveBeenCalledWith('webapp_run', { + app_mode: 'agent-v2', + }) + }) + + it('should track the site response mode without fetching messages for a regular web app', async () => { const handleSend = vi.fn() vi.mocked(useChat).mockReturnValue({ ...defaultChatHookReturn, @@ -768,6 +802,9 @@ describe('ChatWrapper', () => { const options = handleSend.mock.calls[0]![2] expect(options.onGetConversationMessages).toBeUndefined() + expect(mockTrackEvent).toHaveBeenCalledWith('webapp_run', { + app_mode: 'advanced-chat', + }) }) it('should call fetchSuggestedQuestions in doSwitchSibling', async () => { diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 6fee75fdcd0..39a9dc4388e 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -6,6 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { trackEvent } from '@/app/components/base/amplitude' import AnswerIcon from '@/app/components/base/answer-icon' import AppIcon from '@/app/components/base/app-icon' import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form' @@ -221,6 +222,9 @@ const ChatWrapper = () => { onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted, isPublicAPI: appSourceType === AppSourceType.webApp, }) + const appMode = isNewAgent ? 'agent-v2' : appData?.mode + if (appSourceType === AppSourceType.webApp && appMode) + trackEvent('webapp_run', { app_mode: appMode }) }, [ inputsForms, @@ -234,6 +238,7 @@ const ChatWrapper = () => { isHistoryConversation, handleNewConversationCompleted, isNewAgent, + appData?.mode, ], ) diff --git a/web/app/components/base/chat/embedded-chatbot/__tests__/chat-wrapper.spec.tsx b/web/app/components/base/chat/embedded-chatbot/__tests__/chat-wrapper.spec.tsx index 0416d8c0533..f06a28de716 100644 --- a/web/app/components/base/chat/embedded-chatbot/__tests__/chat-wrapper.spec.tsx +++ b/web/app/components/base/chat/embedded-chatbot/__tests__/chat-wrapper.spec.tsx @@ -11,6 +11,12 @@ import { useChat } from '../../chat/hooks' import ChatWrapper from '../chat-wrapper' import { useEmbeddedChatbotContext } from '../context' +const mockTrackEvent = vi.hoisted(() => vi.fn()) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: mockTrackEvent, +})) + vi.mock('../context', () => ({ useEmbeddedChatbotContext: vi.fn(), })) @@ -132,6 +138,7 @@ const createContextValue = ( appMeta: { tool_icons: {} }, appData: { app_id: 'app-1', + mode: 'chat', can_replace_logo: true, custom_config: { remove_webapp_brand: false, @@ -533,6 +540,7 @@ describe('EmbeddedChatbot chat-wrapper', () => { expect(fetchSuggestedQuestions).toHaveBeenCalledWith('resp-2', AppSourceType.tryApp, 'app-1') expect(handleStop).toHaveBeenCalled() expect(screen.queryByRole('img', { name: 'Alice' })).not.toBeInTheDocument() + expect(mockTrackEvent).not.toHaveBeenCalled() cleanup() vi.mocked(useEmbeddedChatbotContext).mockReturnValue( @@ -739,6 +747,9 @@ describe('EmbeddedChatbot chat-wrapper', () => { fireEvent.click(screen.getByRole('button', { name: 'send through chat' })) expect(handleSend).toHaveBeenCalled() + expect(mockTrackEvent).toHaveBeenCalledWith('webapp_run', { + app_mode: 'chat', + }) const options = handleSend.mock.calls[0]?.[2] as { onConversationComplete?: (id: string) => void } diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index e4c089c0d1a..839b2fe3a40 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -6,6 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { trackEvent } from '@/app/components/base/amplitude' import AnswerIcon from '@/app/components/base/answer-icon' import AppIcon from '@/app/components/base/app-icon' import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' @@ -207,6 +208,8 @@ const ChatWrapper = () => { onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, isPublicAPI: appSourceType === AppSourceType.webApp, }) + if (appSourceType === AppSourceType.webApp && appData?.mode) + trackEvent('webapp_run', { app_mode: appData.mode }) }, [ currentConversationId, @@ -217,6 +220,7 @@ const ChatWrapper = () => { appSourceType, appId, handleNewConversationCompleted, + appData?.mode, ], ) diff --git a/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts b/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts index 9de624ca143..8527eaa7f8b 100644 --- a/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts +++ b/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts @@ -12,6 +12,8 @@ import { useResultSender } from '../use-result-sender' const { buildResultRequestDataMock, createWorkflowStreamHandlersMock, + mockTrackEvent, + mockWebAppState, sendCompletionMessageMock, sendWorkflowMessageMock, sleepMock, @@ -19,12 +21,27 @@ const { } = vi.hoisted(() => ({ buildResultRequestDataMock: vi.fn(), createWorkflowStreamHandlersMock: vi.fn(), + mockTrackEvent: vi.fn(), + mockWebAppState: { + appInfo: { + mode: 'completion', + }, + }, sendCompletionMessageMock: vi.fn(), sendWorkflowMessageMock: vi.fn(), sleepMock: vi.fn(), validateResultRequestMock: vi.fn(), })) +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: mockTrackEvent, +})) + +vi.mock('@/context/web-app-context', () => ({ + useWebAppStore: (selector: (state: typeof mockWebAppState) => unknown) => + selector(mockWebAppState), +})) + vi.mock('@/service/share', async () => { const actual = await vi.importActual('@/service/share') return { @@ -226,6 +243,7 @@ const renderSender = ({ describe('useResultSender', () => { beforeEach(() => { vi.clearAllMocks() + mockWebAppState.appInfo.mode = 'completion' validateResultRequestMock.mockReturnValue({ canSend: true }) buildResultRequestDataMock.mockReturnValue({ inputs: { name: 'Alice' } }) createWorkflowStreamHandlersMock.mockReturnValue({ onWorkflowFinished: vi.fn() }) @@ -273,6 +291,7 @@ describe('useResultSender', () => { }) expect(buildResultRequestDataMock).not.toHaveBeenCalled() expect(sendCompletionMessageMock).not.toHaveBeenCalled() + expect(mockTrackEvent).not.toHaveBeenCalled() }) it('should send completion requests when controlSend changes and process callbacks', async () => { @@ -307,6 +326,9 @@ describe('useResultSender', () => { expect(harness.runState.clearMoreLikeThis).toHaveBeenCalledTimes(1) expect(onShowRes).toHaveBeenCalledTimes(1) expect(onRunStart).toHaveBeenCalledTimes(1) + expect(mockTrackEvent).toHaveBeenCalledWith('webapp_run', { + app_mode: 'completion', + }) expect(sendCompletionMessageMock).toHaveBeenCalledWith( { inputs: { name: 'Alice' } }, expect.objectContaining({ @@ -346,6 +368,7 @@ describe('useResultSender', () => { it('should trigger workflow sends on retry and report workflow request failures', async () => { const harness = createRunStateHarness() + mockWebAppState.appInfo.mode = 'workflow' sendWorkflowMessageMock.mockRejectedValue(new Error('workflow failed')) const { rerender, notify } = renderSender({ @@ -385,6 +408,9 @@ describe('useResultSender', () => { }) }) expect(harness.runState.clearMoreLikeThis).not.toHaveBeenCalled() + expect(mockTrackEvent).toHaveBeenCalledWith('webapp_run', { + app_mode: 'workflow', + }) }) it('should configure workflow handlers for installed apps as non-public', async () => { @@ -411,6 +437,7 @@ describe('useResultSender', () => { AppSourceTypeEnum.installedApp, 'app-1', ) + expect(mockTrackEvent).not.toHaveBeenCalled() }) it('should stringify non-Error workflow failures', async () => { diff --git a/web/app/components/share/text-generation/result/hooks/use-result-sender.ts b/web/app/components/share/text-generation/result/hooks/use-result-sender.ts index ba3effe5da6..720e5fe4fe3 100644 --- a/web/app/components/share/text-generation/result/hooks/use-result-sender.ts +++ b/web/app/components/share/text-generation/result/hooks/use-result-sender.ts @@ -4,7 +4,9 @@ import type { ResultRunStateController } from './use-result-run-state' import type { PromptConfig } from '@/models/debug' import type { VisionFile, VisionSettings } from '@/types/app' import { useCallback, useEffect, useRef } from 'react' +import { trackEvent } from '@/app/components/base/amplitude' import { TEXT_GENERATION_TIMEOUT_MS } from '@/config' +import { useWebAppStore } from '@/context/web-app-context' import { AppSourceType, sendCompletionMessage, sendWorkflowMessage } from '@/service/share' import { sleep } from '@/utils' import { buildResultRequestData, validateResultRequest } from '../result-request' @@ -58,6 +60,7 @@ export const useResultSender = ({ visionConfig, }: UseResultSenderOptions) => { const { clearMoreLikeThis } = runState + const appMode = useWebAppStore((state) => state.appInfo?.mode) const handleSend = useCallback(async () => { if (runState.isResponding) { @@ -96,6 +99,9 @@ export const useResultSender = ({ runState.setRespondingTrue() + if (appSourceType === AppSourceType.webApp && appMode) + trackEvent('webapp_run', { app_mode: appMode }) + let isEnd = false let isTimeout = false let completionChunks: string[] = [] @@ -196,6 +202,7 @@ export const useResultSender = ({ return true }, [ appId, + appMode, appSourceType, completionFiles, inputs, diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx index 1f64a7613f1..c147cb2543c 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx @@ -68,6 +68,8 @@ const toastMock = vi.hoisted(() => ({ success: vi.fn(), })) +const trackEventMock = vi.hoisted(() => vi.fn()) + const modelHooksState = vi.hoisted(() => ({ defaultTextGenerationModel: { provider: { @@ -194,6 +196,10 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastMock, })) +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: trackEventMock, +})) + vi.mock('@/service/client', () => ({ consoleQuery: { systemFeatures: { @@ -1252,6 +1258,7 @@ describe('AgentConfigurePage', () => { 'prompt:edited draft prompt', ) expect(mocks.checkoutBuildDraft).not.toHaveBeenCalled() + expect(trackEventMock).not.toHaveBeenCalled() }) it('should stay in Preview when resetting the Build conversation fails', async () => { @@ -1374,6 +1381,7 @@ describe('AgentConfigurePage', () => { expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent( 'draftType:draft', ) + expect(trackEventMock).not.toHaveBeenCalled() expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent( 'readonly:no', ) @@ -1904,7 +1912,7 @@ describe('AgentConfigurePage', () => { expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument() }) - it('should not checkout again when sending build chat from active build draft mode', async () => { + it('should track the run without checking out again in active build draft mode', async () => { const queryClient = new QueryClient() mocks.queryState.composer = { data: { @@ -1955,6 +1963,7 @@ describe('AgentConfigurePage', () => { expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('sent:yes') }) expect(mocks.checkoutBuildDraft).not.toHaveBeenCalled() + expect(trackEventMock).toHaveBeenCalledWith('agent_build_mode_run') }) it('should show the working directory action after the first build reply completes', async () => { diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index e8e340327c8..c5d1a13da47 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -18,6 +18,8 @@ const toastMock = vi.hoisted(() => ({ success: vi.fn(), })) +const trackEventMock = vi.hoisted(() => vi.fn()) + const composerPutMutationFn = vi.hoisted(() => vi.fn( async (variables: { @@ -119,6 +121,10 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ toast: toastMock, })) +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: trackEventMock, +})) + vi.mock('@/service/client', () => ({ consoleQuery: { agent: { @@ -159,9 +165,11 @@ vi.mock('@/service/client', () => ({ })) function renderUseAgentConfigureSync({ + agentName = 'Agent', baseConfig, currentModel, }: { + agentName?: Parameters[0]['agentName'] baseConfig?: Parameters[0]['baseConfig'] currentModel?: Parameters[0]['currentModel'] } = {}) { @@ -183,6 +191,7 @@ function renderUseAgentConfigureSync({ () => useAgentConfigureSync({ agentId: 'agent-1', + agentName, baseConfig, currentModel, enabled: true, @@ -762,6 +771,12 @@ describe('useAgentConfigureSync', () => { active_config_is_published: true, name: 'Agent', }) + expect(trackEventMock).toHaveBeenCalledWith('app_published_time', { + action_mode: 'app', + app_id: 'agent-1', + app_name: 'Agent', + app_mode: 'agent-v2', + }) expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess') }) @@ -781,6 +796,7 @@ describe('useAgentConfigureSync', () => { expect(composerPutMutationFn).not.toHaveBeenCalled() expect(publishAgentMutationFn).not.toHaveBeenCalled() + expect(trackEventMock).not.toHaveBeenCalled() expect(toastMock.error).toHaveBeenCalledWith('common.modelProvider.selectModel') }) diff --git a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx index c0db1aa220c..a845da96da4 100644 --- a/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/composer-session.tsx @@ -14,6 +14,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import { ScopeProvider } from 'jotai-scope' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { trackEvent } from '@/app/components/base/amplitude' import Loading from '@/app/components/base/loading' import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' @@ -328,6 +329,7 @@ function AgentConfigurePageComposerContent({ useAgentConfigureModelOptions() const { draftSavedAt, isPublishing, publishDraft, saveDraft } = useAgentConfigureSync({ agentId, + agentName: agentQuery.data?.name, baseConfig: agentSoulConfig, currentModel, enabled: composerQuery.isSuccess && !selectedVersionId && !buildDraft.isActive, @@ -564,11 +566,13 @@ function AgentConfigurePageComposerContent({ throw new Error('Agent model is required.') } - return runBuildPreparation({ + const preparedBuildDraft = await runBuildPreparation({ generation: buildCallbackGeneration, markBuildChatStarted: true, prepare: buildDraftActions.prepareBuildDraftBeforeRun, }) + trackEvent('agent_build_mode_run') + return preparedBuildDraft } : saveDraft } diff --git a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts index b2c5b06c427..4496ff774c3 100644 --- a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts +++ b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts @@ -10,6 +10,7 @@ import isEqual from 'fast-deep-equal' import { useSetAtom, useStore } from 'jotai' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { trackEvent } from '@/app/components/base/amplitude' import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/conversions' import { @@ -29,11 +30,13 @@ const DRAFT_AUTOSAVE_WAIT = 5000 export function useAgentConfigureSync({ agentId, + agentName, baseConfig, currentModel, enabled, }: { agentId: string + agentName?: string | null baseConfig?: AgentSoulConfig currentModel?: DefaultModel enabled: boolean @@ -303,6 +306,12 @@ export function useAgentConfigureSync({ const publishedDraft = draft setOriginalDraft(publishedDraft) setPublishedDraft(publishedDraft) + trackEvent('app_published_time', { + action_mode: 'app', + app_id: agentId, + app_name: agentName, + app_mode: 'agent-v2', + }) toast.success(tCommon(($) => $['api.actionSuccess'])) } finally { publishInFlightRef.current = false @@ -310,6 +319,7 @@ export function useAgentConfigureSync({ } }, [ agentId, + agentName, debouncedSaveDraft, getKnowledgeValidationMessage, publishAgent, diff --git a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx index bb07a3ac588..d5ca96f19a7 100644 --- a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx @@ -14,6 +14,8 @@ const toastMock = vi.hoisted(() => ({ const routerPushMock = vi.hoisted(() => vi.fn()) +const trackCreateAppMock = vi.hoisted(() => vi.fn()) + vi.mock('@tanstack/react-query', () => ({ useMutation: () => ({ isPending: mutationMock.isPending, @@ -31,6 +33,10 @@ vi.mock('@/next/navigation', () => ({ }), })) +vi.mock('@/utils/create-app-tracking', () => ({ + trackCreateApp: trackCreateAppMock, +})) + vi.mock('@/service/client', () => ({ consoleQuery: { agent: { @@ -106,6 +112,10 @@ describe('CreateAgentDialog', () => { }) expect(toastMock.success).toHaveBeenCalledWith('agentV2.roster.createSuccess') + expect(trackCreateAppMock).toHaveBeenCalledWith({ + source: 'studio_blank', + appMode: 'agent-v2', + }) expect(routerPushMock).toHaveBeenCalledWith('/agents/agent-1/configure') }) diff --git a/web/features/agent-v2/roster/components/create-agent-dialog.tsx b/web/features/agent-v2/roster/components/create-agent-dialog.tsx index b7a593f684d..274fc4d0908 100644 --- a/web/features/agent-v2/roster/components/create-agent-dialog.tsx +++ b/web/features/agent-v2/roster/components/create-agent-dialog.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next' import AppIconPicker from '@/app/components/base/app-icon-picker' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/client' +import { trackCreateApp } from '@/utils/create-app-tracking' import { getAgentDetailPath } from '../../agent-detail/routes' import { defaultAgentIcon } from './agent-form' import { AgentFormFields } from './agent-form-fields' @@ -76,6 +77,10 @@ export function CreateAgentDialog({ open, onOpenChange }: CreateAgentDialogProps }, { onSuccess: (createdAgent) => { + trackCreateApp({ + source: 'studio_blank', + appMode: 'agent-v2', + }) toast.success(t(($) => $['roster.createSuccess'])) handleOpenChange(false) router.push(getAgentDetailPath(createdAgent.id, 'configure')) diff --git a/web/models/share.ts b/web/models/share.ts index 16d5a17f1c4..28463103d04 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -1,3 +1,4 @@ +import type { AppMode } from '@dify/contracts/api/web/types.gen' import type { Locale } from '@/i18n-config' import type { AppIconType } from '@/types/app' @@ -36,6 +37,7 @@ export type AppMeta = { export type CustomConfigValueType = string | number | boolean | null | undefined export type AppData = { app_id: string + mode?: AppMode can_replace_logo?: boolean custom_config: Record | null enable_site?: boolean diff --git a/web/utils/__tests__/create-app-tracking.spec.ts b/web/utils/__tests__/create-app-tracking.spec.ts index 02bb6367469..0790ceee71e 100644 --- a/web/utils/__tests__/create-app-tracking.spec.ts +++ b/web/utils/__tests__/create-app-tracking.spec.ts @@ -141,6 +141,23 @@ describe('create-app-tracking', () => { }) }) + it('should preserve agent v2 mode as its own app mode', () => { + expect( + buildCreateAppEventPayload( + { + source: 'studio_blank', + appMode: 'agent-v2', + }, + null, + new Date(2026, 3, 13, 9, 8, 9), + ), + ).toEqual({ + source: 'studio_blank', + app_mode: 'agent-v2', + time: '04-13-09:08:09', + }) + }) + it('should fold legacy non-agent modes into chatflow', () => { expect( buildCreateAppEventPayload( diff --git a/web/utils/create-app-tracking.ts b/web/utils/create-app-tracking.ts index 030e6e7c6cb..37e4cedc580 100644 --- a/web/utils/create-app-tracking.ts +++ b/web/utils/create-app-tracking.ts @@ -18,7 +18,7 @@ type SearchParamReader = { get: (name: string) => string | null } -type OriginalCreateAppMode = 'workflow' | 'chatflow' | 'agent' +type OriginalCreateAppMode = 'workflow' | 'chatflow' | 'agent' | 'agent-v2' type CreateAppSource = | 'external' @@ -78,6 +78,8 @@ const formatCreateAppTime = (date: Date) => { const mapOriginalCreateAppMode = (appMode: string): OriginalCreateAppMode => { if (appMode === AppModeEnum.WORKFLOW) return 'workflow' + if (appMode === 'agent-v2') return 'agent-v2' + if (appMode === AppModeEnum.AGENT_CHAT || appMode === 'agent') return 'agent' return 'chatflow'