feat: track inline agent and preview mode events (#41373)

This commit is contained in:
Joel
2026-08-27 08:09:09 +00:00
committed by GitHub
parent 1db542c706
commit d94204ec33
11 changed files with 137 additions and 7 deletions
@@ -1,11 +1,14 @@
import { QueryClient } from '@tanstack/react-query'
import { act, waitFor } from '@testing-library/react'
import { getDefaultStore } from 'jotai'
import { useStore as useAppStore } from '@/app/components/app/store'
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import {
agentComposerDraftAtom,
agentComposerSavedDraftAtom,
} from '@/features/agent-v2/agent-composer/store'
import { AgentScope } from '@/features/agent-v2/analytics'
import { AppModeEnum } from '@/types/app'
import { FlowType } from '@/types/common'
import { renderWorkflowHook } from '../../../__tests__/workflow-test-env'
import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config'
@@ -128,6 +131,7 @@ const mockSnippetComposerQueryOptions = vi.hoisted(() =>
},
),
)
const trackCreateAppMock = vi.hoisted(() => vi.fn())
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
@@ -141,6 +145,10 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
}),
}))
vi.mock('@/utils/create-app-tracking', () => ({
trackCreateApp: trackCreateAppMock,
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
@@ -318,6 +326,7 @@ describe('useWorkflowInlineAgentDetail', () => {
describe('useCreateInlineAgentBinding', () => {
beforeEach(() => {
vi.clearAllMocks()
useAppStore.getState().setAppDetail({ mode: AppModeEnum.WORKFLOW } as never)
mockDefaultModel.value = {
model: 'gpt-4o-mini',
model_type: 'llm',
@@ -405,6 +414,44 @@ describe('useCreateInlineAgentBinding', () => {
}),
}),
)
expect(trackCreateAppMock).toHaveBeenCalledWith({
source: 'studio_blank',
appMode: 'agent-v2',
agentScope: AgentScope.InWorkflow,
})
})
it('tracks inline agent creation with the chatflow scope', async () => {
useAppStore.getState().setAppDetail({ mode: AppModeEnum.ADVANCED_CHAT } as never)
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
})
const { result } = renderWorkflowHook(() => useCreateInlineAgentBinding(), {
queryClient,
hooksStoreProps: {
configsMap: {
flowId: 'chatflow-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
},
})
act(() => {
void result.current.createInlineAgentBinding('node-1')
})
await waitFor(() => {
expect(trackCreateAppMock).toHaveBeenCalledWith({
source: 'studio_blank',
appMode: 'agent-v2',
agentScope: AgentScope.InChatflow,
})
})
})
it('creates inline agent through the snippet composer API', async () => {
@@ -6,7 +6,10 @@ import type { ReactNode, Ref } from 'react'
import type { AgentBuildDraftChangeSummary } from '@/features/agent-v2/agent-detail/configure/components/orchestrate/build-draft-changes-context'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AgentScope } from '@/features/agent-v2/analytics'
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
import { FlowType } from '@/types/common'
import { WorkflowInlineAgentConfigureWorkspace } from '../agent-orchestrate-panel-content'
@@ -24,6 +27,7 @@ const mocks = vi.hoisted(() => ({
saveAgentSoulConfig: vi.fn(),
saveDraft: vi.fn(),
stopBuildChat: vi.fn(),
trackEvent: vi.fn(),
uploadAgentSandboxFile: vi.fn(),
uploadWorkflowSandboxFile: vi.fn(),
}))
@@ -34,6 +38,10 @@ vi.mock('@/features/agent-v2/permissions', () => ({
useCanManageAgents: () => permission.canManageAgents,
}))
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: mocks.trackEvent,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useDefaultModel: () => ({
data: undefined,
@@ -486,6 +494,7 @@ function createDeferredPromise<T>() {
describe('WorkflowInlineAgentConfigureWorkspace', () => {
beforeEach(() => {
vi.clearAllMocks()
useAppStore.getState().setAppDetail({ mode: AppModeEnum.WORKFLOW } as never)
mocks.completeBuildConversation = undefined
permission.canManageAgents = true
mocks.loadBuildDraft.mockRejectedValue(new Response(null, { status: 404 }))
@@ -595,6 +604,9 @@ describe('WorkflowInlineAgentConfigureWorkspace', () => {
await user.click(screen.getByRole('button', { name: 'send preview message' }))
await waitFor(() => expect(mocks.saveDraft).toHaveBeenCalled())
expect(mocks.trackEvent).toHaveBeenCalledWith('agent_preview_mode_run', {
agent_scope: AgentScope.InWorkflow,
})
expect(mocks.saveBuildDraft).not.toHaveBeenCalled()
await user.click(
@@ -967,6 +979,9 @@ describe('WorkflowInlineAgentConfigureWorkspace', () => {
expect(saveDraftCallOrder).toBeLessThan(saveBuildDraftCallOrder)
expect(mocks.checkoutBuildDraft).not.toHaveBeenCalled()
expect(mocks.trackEvent).toHaveBeenCalledWith('agent_build_mode_run', {
agent_scope: AgentScope.InWorkflow,
})
})
it('should use the saved build draft response as the build chat source', async () => {
@@ -65,6 +65,11 @@ import {
useAgentConfigureBuildDraftData,
} from '@/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft'
import { useAgentConfigureSessionController } from '@/features/agent-v2/agent-detail/configure/use-agent-configure-session-controller'
import {
trackAgentBuildModeRun,
trackAgentPreviewModeRun,
useInlineAgentScope,
} from '@/features/agent-v2/analytics'
import { useCanManageAgents } from '@/features/agent-v2/permissions'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
@@ -294,6 +299,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
}) {
const { t } = useTranslation('common')
const { t: tAgent } = useTranslation('agentV2')
const agentScope = useInlineAgentScope()
const queryClient = useQueryClient()
const jotaiStore = useJotaiStore()
const setBuildDraftSoulSourceOverride = buildDraft.setSoulSourceOverride
@@ -779,15 +785,18 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
}}
onSaveDraftBeforeRun={
rightPanelMode === 'build'
? () => {
return runBuildPreparation({
? async () => {
const preparedBuildDraft = await runBuildPreparation({
generation: buildCallbackGeneration,
markBuildChatStarted: true,
prepare: prepareInlineBuildDraftBeforeRun,
})
trackAgentBuildModeRun(agentScope)
return preparedBuildDraft
}
: async () => {
await saveDraft()
trackAgentPreviewModeRun(agentScope)
}
}
onSendInterrupted={() => {
@@ -6,8 +6,10 @@ import { useTranslation } from 'react-i18next'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { useInlineAgentScope } from '@/features/agent-v2/analytics'
import { consoleQuery } from '@/service/client'
import { FlowType } from '@/types/common'
import { trackCreateApp } from '@/utils/create-app-tracking'
import { getDefaultAgentSoul } from './agent-soul-config'
type CreatedInlineAgentBinding = AgentInlineBinding & {
@@ -99,6 +101,7 @@ export function useWorkflowInlineAgentDetail(
export function useCreateInlineAgentBinding() {
const { t } = useTranslation('agentV2')
const configsMap = useHooksStore((state) => state.configsMap)
const agentScope = useInlineAgentScope()
const { data: defaultModel } = useDefaultModel(ModelTypeEnum.textGeneration)
const queryClient = useQueryClient()
const { isPending: isAppComposerPending, mutateAsync: mutateAppComposerAsync } = useMutation(
@@ -187,6 +190,11 @@ export function useCreateInlineAgentBinding() {
composerState,
)
}
trackCreateApp({
source: 'studio_blank',
appMode: 'agent-v2',
agentScope,
})
options?.onSuccess?.({
binding_type: 'inline_agent',
agent_id: binding.agent_id,
@@ -197,6 +205,7 @@ export function useCreateInlineAgentBinding() {
}
},
[
agentScope,
configsMap?.flowId,
configsMap?.flowType,
defaultModel,
@@ -4,6 +4,7 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ScopeProvider } from 'jotai-scope'
import { useState } from 'react'
import { AgentScope } from '@/features/agent-v2/analytics'
import { renderWithNuqs as render } from '@/test/nuqs-testing'
import { AgentConfigureComposerScope } from '../components/composer-session'
import { useAgentConfigureData } from '../hooks'
@@ -1429,7 +1430,9 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent(
'draftType:draft',
)
expect(trackEventMock).not.toHaveBeenCalled()
expect(trackEventMock).toHaveBeenCalledWith('agent_preview_mode_run', {
agent_scope: AgentScope.Global,
})
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
'readonly:no',
)
@@ -2011,7 +2014,9 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('sent:yes')
})
expect(mocks.checkoutBuildDraft).not.toHaveBeenCalled()
expect(trackEventMock).toHaveBeenCalledWith('agent_build_mode_run')
expect(trackEventMock).toHaveBeenCalledWith('agent_build_mode_run', {
agent_scope: AgentScope.Global,
})
})
it('should show the working directory action after the first build reply completes', async () => {
@@ -14,11 +14,15 @@ import { useAtomValue, useSetAtom } from 'jotai'
import { ScopeProvider } from 'jotai-scope'
import { useCallback, useEffect, 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'
import { rebaseAgentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
import {
AgentScope,
trackAgentBuildModeRun,
trackAgentPreviewModeRun,
} from '@/features/agent-v2/analytics'
import { consoleQuery } from '@/service/client'
import { useAgentConfigureModelOptions } from '../hooks'
import {
@@ -588,10 +592,13 @@ function AgentConfigurePageComposerContent({
markBuildChatStarted: true,
prepare: buildDraftActions.prepareBuildDraftBeforeRun,
})
trackEvent('agent_build_mode_run')
trackAgentBuildModeRun(AgentScope.Global)
return preparedBuildDraft
}
: saveDraft
: async () => {
await saveDraft()
trackAgentPreviewModeRun(AgentScope.Global)
}
}
onSendInterrupted={() => {
if (rightPanelChatMode === 'build') finishBuildAction(buildCallbackGeneration)
+28
View File
@@ -0,0 +1,28 @@
import { useStore as useAppStore } from '@/app/components/app/store'
import { trackEvent } from '@/app/components/base/amplitude'
import { AppModeEnum } from '@/types/app'
export type AgentScope = (typeof AgentScope)[keyof typeof AgentScope]
export const AgentScope = {
InWorkflow: 'in_workflow',
InChatflow: 'in_chatflow',
Global: 'global',
} as const
export const useInlineAgentScope = () => {
const appMode = useAppStore((state) => state.appDetail?.mode)
return appMode === AppModeEnum.ADVANCED_CHAT ? AgentScope.InChatflow : AgentScope.InWorkflow
}
export const trackAgentBuildModeRun = (agentScope: AgentScope) => {
return trackEvent('agent_build_mode_run', {
agent_scope: agentScope,
})
}
export const trackAgentPreviewModeRun = (agentScope: AgentScope) => {
return trackEvent('agent_preview_mode_run', {
agent_scope: agentScope,
})
}
@@ -1,5 +1,6 @@
import { act, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentScope } from '@/features/agent-v2/analytics'
import { CreateAgentDialog } from '../create-agent-dialog'
const mutationMock = vi.hoisted(() => ({
@@ -115,6 +116,7 @@ describe('CreateAgentDialog', () => {
expect(trackCreateAppMock).toHaveBeenCalledWith({
source: 'studio_blank',
appMode: 'agent-v2',
agentScope: AgentScope.Global,
})
expect(routerPushMock).toHaveBeenCalledWith('/agents/agent-1/configure')
})
@@ -17,6 +17,7 @@ import { useMutation } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import { AgentScope } from '@/features/agent-v2/analytics'
import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { trackCreateApp } from '@/utils/create-app-tracking'
@@ -80,6 +81,7 @@ export function CreateAgentDialog({ open, onOpenChange }: CreateAgentDialogProps
trackCreateApp({
source: 'studio_blank',
appMode: 'agent-v2',
agentScope: AgentScope.Global,
})
toast.success(t(($) => $['roster.createSuccess']))
handleOpenChange(false)
@@ -1,6 +1,7 @@
import Cookies from 'js-cookie'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import * as amplitude from '@/app/components/base/amplitude/utils'
import { AgentScope } from '@/features/agent-v2/analytics'
import { AppModeEnum } from '@/types/app'
import {
buildCreateAppEventPayload,
@@ -147,6 +148,7 @@ describe('create-app-tracking', () => {
{
source: 'studio_blank',
appMode: 'agent-v2',
agentScope: AgentScope.Global,
},
null,
new Date(2026, 3, 13, 9, 8, 9),
@@ -154,6 +156,7 @@ describe('create-app-tracking', () => {
).toEqual({
source: 'studio_blank',
app_mode: 'agent-v2',
agent_scope: 'global',
time: '04-13-09:08:09',
})
})
+3
View File
@@ -1,3 +1,4 @@
import type { AgentScope } from '@/features/agent-v2/analytics'
import Cookies from 'js-cookie'
import { flushEvents, trackEvent } from '@/app/components/base/amplitude/utils'
import { AppModeEnum } from '@/types/app'
@@ -32,6 +33,7 @@ type CreateAppSource =
export type TrackCreateAppParams = {
source: CreateAppSource
appMode: string
agentScope?: AgentScope
templateId?: string
}
@@ -177,6 +179,7 @@ export const buildCreateAppEventPayload = (
source,
app_mode: mapOriginalCreateAppMode(params.appMode),
time: formatCreateAppTime(currentTime),
...(params.agentScope ? { agent_scope: params.agentScope } : {}),
...(params.templateId ? { template_id: params.templateId } : {}),
...(externalAttribution
? {