feat(web): add tracking for app preview events in AppCard component (#35347)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-04-17 11:37:24 +08:00
committed by GitHub
parent b565a51ed9
commit a5ce17009f
4 changed files with 85 additions and 6 deletions

View File

@@ -3,6 +3,8 @@ import type { App } from '@/models/explore'
import type { AppIconType } from '@/types/app'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { trackEvent } from '@/app/components/base/amplitude'
import AppListContext from '@/context/app-list-context'
import { AppModeEnum } from '@/types/app'
import AppCard from '../index'
@@ -10,6 +12,10 @@ vi.mock('@heroicons/react/20/solid', () => ({
PlusIcon: ({ className }: any) => <div data-testid="plus-icon" className={className} aria-label="Add icon">+</div>,
}))
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
const mockApp: App = {
can_trial: true,
app: {
@@ -38,12 +44,30 @@ const mockApp: App = {
}
describe('AppCard', () => {
const mockSetShowTryAppPanel = vi.fn()
const mockTrackEvent = vi.mocked(trackEvent)
const defaultProps = {
app: mockApp,
canCreate: true,
onCreate: vi.fn(),
}
const renderWithProvider = (ui: React.ReactElement) => {
return render(
// eslint-disable-next-line react/no-context-provider
<AppListContext.Provider
value={{
currentApp: undefined,
isShowTryAppPanel: false,
setShowTryAppPanel: mockSetShowTryAppPanel,
controlHideCreateFromTemplatePanel: 0,
}}
>
{ui}
</AppListContext.Provider>,
)
}
beforeEach(() => {
vi.clearAllMocks()
})
@@ -217,6 +241,25 @@ describe('AppCard', () => {
// Note: Card click doesn't trigger onCreate, only the button does
expect(mockOnCreate).not.toHaveBeenCalled()
})
it('should track preview event and open try app panel when detail button is clicked', async () => {
renderWithProvider(<AppCard {...defaultProps} />)
const button = screen.getByRole('button', { name: /explore\.appCard\.try/ })
await userEvent.click(button)
expect(mockTrackEvent).toHaveBeenCalledWith('preview_template', {
template_id: mockApp.app_id,
template_name: mockApp.app.name,
template_mode: mockApp.app.mode,
template_category: mockApp.category,
page: 'studio',
})
expect(mockSetShowTryAppPanel).toHaveBeenCalledWith(true, {
appId: mockApp.app_id,
app: mockApp,
})
})
})
describe('Keyboard Accessibility', () => {

View File

@@ -6,6 +6,7 @@ import { RiInformation2Line } from '@remixicon/react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import { trackEvent } from '@/app/components/base/amplitude'
import AppIcon from '@/app/components/base/app-icon'
import { Button } from '@/app/components/base/ui/button'
import AppListContext from '@/context/app-list-context'
@@ -28,11 +29,16 @@ const AppCard = ({
const { systemFeatures } = useGlobalPublicStore()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
const showTryAPPPanel = useCallback((appId: string) => {
return () => {
setShowTryAppPanel?.(true, { appId, app })
}
}, [setShowTryAppPanel, app.category])
const handleShowTryAppPanel = useCallback(() => {
trackEvent('preview_template', {
template_id: app.app_id,
template_name: appBasicInfo.name,
template_mode: appBasicInfo.mode,
template_category: app.category,
page: 'studio',
})
setShowTryAppPanel?.(true, { appId: app.app_id, app })
}, [setShowTryAppPanel, app, appBasicInfo])
return (
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
<div className="flex shrink-0 grow-0 items-center gap-3 pb-2">
@@ -71,7 +77,7 @@ const AppCard = ({
<span className="text-xs">{t('newApp.useTemplate', { ns: 'app' })}</span>
</Button>
)}
<Button onClick={showTryAPPPanel(app.app_id)}>
<Button onClick={handleShowTryAppPanel}>
<RiInformation2Line className="mr-1 size-4" />
<span>{t('appCard.try', { ns: 'explore' })}</span>
</Button>

View File

@@ -2,6 +2,7 @@ import type { AppCardProps } from '../index'
import type { App } from '@/models/explore'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { trackEvent } from '@/app/components/base/amplitude'
import { AppModeEnum } from '@/types/app'
import AppCard from '../index'
@@ -9,6 +10,10 @@ vi.mock('../../../app/type-selector', () => ({
AppTypeIcon: ({ type }: { type: string }) => <div data-testid="app-type-icon">{type}</div>,
}))
vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
const createApp = (overrides?: Partial<App>): App => ({
can_trial: true,
app_id: 'app-id',
@@ -41,6 +46,7 @@ const createApp = (overrides?: Partial<App>): App => ({
describe('AppCard', () => {
const onCreate = vi.fn()
const onTry = vi.fn()
const mockTrackEvent = vi.mocked(trackEvent)
const renderComponent = (props?: Partial<AppCardProps>) => {
const mergedProps: AppCardProps = {
@@ -148,5 +154,21 @@ describe('AppCard', () => {
expect(onTry).toHaveBeenCalledWith({ appId: 'app-id', app })
})
it('should track preview event when detail button is clicked', () => {
const app = createApp()
renderComponent({ app, canCreate: true, isExplore: true })
fireEvent.click(screen.getByText('explore.appCard.try'))
expect(mockTrackEvent).toHaveBeenCalledWith('preview_template', {
template_id: app.app_id,
template_name: app.app.name,
template_mode: app.app.mode,
template_category: app.category,
page: 'explore',
})
})
})
})

View File

@@ -5,6 +5,7 @@ import { PlusIcon } from '@heroicons/react/20/solid'
import { cn } from '@langgenius/dify-ui/cn'
import { RiInformation2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import AppIcon from '@/app/components/base/app-icon'
import { Button } from '@/app/components/base/ui/button'
import { useGlobalPublicStore } from '@/context/global-public-context'
@@ -31,6 +32,13 @@ const AppCard = ({
const { systemFeatures } = useGlobalPublicStore()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const handleTryApp = () => {
trackEvent('preview_template', {
template_id: app.app_id,
template_name: appBasicInfo.name,
template_mode: appBasicInfo.mode,
template_category: app.category,
page: 'explore',
})
onTry({ appId: app.app_id, app })
}