From a5ce17009f521682efd001aca8c506bddcebf377 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Fri, 17 Apr 2026 11:37:24 +0800 Subject: [PATCH] feat(web): add tracking for app preview events in AppCard component (#35347) Co-authored-by: CodingOnStar --- .../app-card/__tests__/index.spec.tsx | 43 +++++++++++++++++++ .../app/create-app-dialog/app-card/index.tsx | 18 +++++--- .../explore/app-card/__tests__/index.spec.tsx | 22 ++++++++++ web/app/components/explore/app-card/index.tsx | 8 ++++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx index 898bde5c71..2d76c12b68 100644 --- a/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx @@ -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) =>
+
, })) +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 + + {ui} + , + ) + } + 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() + + 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', () => { diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx index 9241461608..fef7199ca2 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.tsx @@ -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 (
@@ -71,7 +77,7 @@ const AppCard = ({ {t('newApp.useTemplate', { ns: 'app' })} )} - diff --git a/web/app/components/explore/app-card/__tests__/index.spec.tsx b/web/app/components/explore/app-card/__tests__/index.spec.tsx index 2180980ee9..c42f6f1d32 100644 --- a/web/app/components/explore/app-card/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-card/__tests__/index.spec.tsx @@ -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 }) =>
{type}
, })) +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: vi.fn(), +})) + const createApp = (overrides?: Partial): App => ({ can_trial: true, app_id: 'app-id', @@ -41,6 +46,7 @@ const createApp = (overrides?: Partial): App => ({ describe('AppCard', () => { const onCreate = vi.fn() const onTry = vi.fn() + const mockTrackEvent = vi.mocked(trackEvent) const renderComponent = (props?: Partial) => { 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', + }) + }) }) }) diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx index d88eddc2cf..00e21de7d6 100644 --- a/web/app/components/explore/app-card/index.tsx +++ b/web/app/components/explore/app-card/index.tsx @@ -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 }) }