feat: track gtm v2 events (#5733)

* fix(gtm): delay gtm script load so we can mount script after config init

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): add gtm v2 config and script

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(gtm): incorrect literal constraint for event fields

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(gtm): type refining for gtm package

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): add gtm v2 event trackers

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(applaunchpad): add gtmv2 config and scripts

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(applaunchpad): add gtmv2 events

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(applaunchpad): add gtmv2 error and paywall event

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): workspace toggle not closed when workspace popup is open

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(applaunchpad): missing default config for gtm id

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(applaunchpad): add gtmv2 terminal open event

Signed-off-by: Nixieboluo <me@sagirii.me>

---------

Signed-off-by: Nixieboluo <me@sagirii.me>
This commit is contained in:
Nixieboluo
2025-07-31 14:33:31 +08:00
committed by Yun Pan
parent 0ffe1d6bd0
commit 37fa4ae12d
30 changed files with 401 additions and 119 deletions
+1
View File
@@ -40,6 +40,7 @@ desktop:
docsUrl: "https://sealos.run/docs/Intro/"
aiAssistantEnabled: false
bannerEnabled: false
gtmId: null
auth:
proxyAddress: ""
callbackURL: "https://127.0.0.1.nip.io/callback"
@@ -10,7 +10,6 @@ import { MouseEvent, useContext, useMemo, useRef, useState } from 'react';
import { useContextMenu } from 'react-contexify';
import { ChevronDownIcon } from '../icons';
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
import { gtmOpenCostcenter } from '@/utils/gtm';
const APP_DOCK_MENU_ID = 'APP_DOCK_MENU_ID';
@@ -255,9 +254,6 @@ export default function AppDock() {
moreAppsContent?.setShowMoreApps(true);
return;
}
if (item.key === 'system-costcenter') {
gtmOpenCostcenter();
}
if (item.pid === currentAppPid && item.size !== 'minimize') {
updateOpenedAppInfo({
...item,
@@ -16,7 +16,7 @@ import { ArrowLeft, ChevronLeft, ChevronRight, CircleAlert, X } from 'lucide-rea
import { useQuery } from '@tanstack/react-query';
import { UserInfo } from '@/api/auth';
import useSessionStore from '@/stores/session';
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import useAppStore from '@/stores/app';
import {
devboxDriverObj,
@@ -29,6 +29,7 @@ import {
import { WindowSize } from '@/types';
import { Image } from '@chakra-ui/react';
import { useGuideModalStore } from '@/stores/guideModal';
import { track } from '@sealos/gtm';
const GuideModal = () => {
const { t } = useTranslation();
@@ -45,6 +46,15 @@ const GuideModal = () => {
setInitGuide
} = useGuideModalStore();
useEffect(() => {
if (isOpen) {
track('module_open', {
module: 'guide',
trigger: guideModalInitGuide ? 'onboarding' : 'manual'
});
}
}, [isOpen, guideModalInitGuide]);
const infoData = useQuery({
queryFn: UserInfo,
queryKey: [session?.token, 'UserInfo'],
@@ -278,6 +288,11 @@ const GuideModal = () => {
setInitGuide(false);
closeGuideModal();
startDriver(quitGuideDriverObj(t));
track('guide_exit', {
module: 'guide',
progress_step: activeStep
});
};
return (
@@ -332,12 +347,28 @@ const GuideModal = () => {
switch (cur.key) {
case 'system-applaunchpad':
track('guide_start', {
module: 'guide',
guide_name: 'applaunchpad'
});
return startDriver(appLaunchpadDriverObj(openDesktopApp, t));
case 'system-template':
track('guide_start', {
module: 'guide',
guide_name: 'appstore'
});
return startDriver(templateDriverObj(openDesktopApp, t));
case 'system-dbprovider':
track('guide_start', {
module: 'guide',
guide_name: 'database'
});
return startDriver(databaseDriverObj(openDesktopApp, t));
case 'system-devbox':
track('guide_start', {
module: 'guide',
guide_name: 'devbox'
});
return startDriver(devboxDriverObj(openDesktopApp, t));
default:
return;
@@ -34,6 +34,7 @@ import styles from './index.module.scss';
import { ArrowRight, Volume2 } from 'lucide-react';
import { useGuideModalStore } from '@/stores/guideModal';
import { currentDriver, destroyDriver } from '../account/driver';
import { track } from '@sealos/gtm';
const AppItem = ({
app,
@@ -795,7 +796,23 @@ export default function Apps() {
gap={'8px'}
p={'8px 12px'}
cursor={'pointer'}
onClick={layoutConfig?.version === 'cn' ? openReferralApp : () => openGuideModal()}
onClick={
layoutConfig?.version === 'cn'
? () => {
track('announcement_click', {
module: 'dashboard',
announcement_id: 'invitation_referral_prompt'
});
openReferralApp();
}
: () => {
track('announcement_click', {
module: 'dashboard',
announcement_id: 'onboarding_guide_prompt'
});
openGuideModal();
}
}
>
<Box position="relative" className="gradient-icon">
<Volume2 width={16} height={16} />
@@ -16,6 +16,7 @@ import { createRequest } from '@/api/namespace';
import { useCustomToast } from '@/hooks/useCustomToast';
import { ApiResp } from '@/types';
import { useTranslation } from 'next-i18next';
import { track } from '@sealos/gtm';
export default function CreateTeam({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const { t } = useTranslation();
@@ -29,6 +30,9 @@ export default function CreateTeam({ isOpen, onClose }: { isOpen: boolean; onClo
onSuccess(data) {
if (data.code === 200) {
queryClient.invalidateQueries({ queryKey: ['teamList'] });
track('workspace_create', {
module: 'workspace'
});
onClose();
}
},
@@ -20,6 +20,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import CustomInput from './Input';
import { track } from '@sealos/gtm';
export default function DissolveTeam({
nsid,
ns_uid,
@@ -43,6 +44,9 @@ export default function DissolveTeam({
queryKey: ['teamList'],
exact: false
});
track('workspace_delete', {
module: 'workspace'
});
onSuccess && onSuccess(ns_uid);
onClose();
},
@@ -29,6 +29,7 @@ import { ApiResp } from '@/types';
import { useTranslation } from 'next-i18next';
import { GroupAddIcon } from '@sealos/ui';
import { useCopyData } from '@/hooks/useCopyData';
import { track } from '@sealos/gtm';
export default function InviteMember({
ns_uid,
@@ -68,6 +69,12 @@ export default function InviteMember({
const getLinkCode = useMutation({
mutationFn: getInviteCodeRequest,
mutationKey: [session?.user.ns_uid],
onSuccess(_data, variables) {
track('workspace_invite', {
module: 'workspace',
invite_role: variables.role === UserRole.Developer ? 'developer' : 'manager'
});
},
onError() {
toast({
status: 'error',
@@ -37,6 +37,7 @@ import NsListItem from '@/components/team/NsListItem';
import RenameTeam from './RenameTeam';
import { Plus, Settings } from 'lucide-react';
import useAppStore from '@/stores/app';
import { track } from '@sealos/gtm';
export default function TeamCenter({
isOpen,
@@ -108,6 +109,15 @@ export default function TeamCenter({
}
}, [_namespaces, ns_uid]);
useEffect(() => {
if (isOpen) {
track('module_view', {
view_name: 'manage',
module: 'workspace'
});
}
}, [isOpen]);
const openAccountCenterApp = (page?: string) => {
openDesktopApp({
appKey: 'system-account-center',
@@ -27,6 +27,8 @@ import { useRouter } from 'next/router';
import { ChevronDown, Plus, Settings } from 'lucide-react';
import CreateTeam from './CreateTeam';
import BoringAvatar from 'boring-avatars';
import { useEffect, useMemo, useRef } from 'react';
import { track } from '@sealos/gtm';
export default function WorkspaceToggle() {
const modalDisclosure = useDisclosure();
@@ -41,6 +43,10 @@ export default function WorkspaceToggle() {
mutationFn: switchRequest,
async onSuccess(data) {
if (data.code === 200 && !!data.data && session) {
track('workspace_switch', {
module: 'workspace'
});
const payload = jwtDecode<AccessTokenPayload>(data.data.token);
await sessionConfig({
...data.data,
@@ -63,10 +69,106 @@ export default function WorkspaceToggle() {
const namespaces = data?.data?.namespaces || [];
const namespace = namespaces.find((x) => x.uid === ns_uid);
const WorkspaceList = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => {
// Prevent unwanted excess events
const prevIsOpenRef = useRef<boolean>(true);
useEffect(() => {
if (isOpen && !prevIsOpenRef.current) {
track('module_open', {
module: 'workspace'
});
}
prevIsOpenRef.current = isOpen;
}, [isOpen]);
return (
<PopoverContent w={'full'} minW={'274px'}>
<PopoverBody
cursor={'initial'}
// maxH={'300px'}
// overflow={'auto'}
borderRadius={'12px'}
p={'0'}
py={'8px'}
color={'#18181B'}
style={{
scrollbarWidth: 'none'
}}
fontSize={'13px'}
>
<Text px={'12px'} py={'6px'} color={'#71717A'} fontSize={'12px'} fontWeight={'500'}>
{t('common:workspace')}
</Text>
<VStack gap={0} alignItems={'stretch'} maxH={'260px'} overflow={'scroll'}>
{namespaces.map((ns) => {
return (
<NsListItem
key={ns.uid}
width={'full'}
onClick={() => {
switchTeam({ uid: ns.uid });
}}
displayPoint={true}
id={ns.uid}
isPrivate={ns.nstype === NSType.Private}
isSelected={ns.uid === ns_uid}
teamName={ns.teamName}
teamAvatar={ns.id}
showCheck={true}
selectedColor={'rgba(0, 0, 0, 0.05)'}
fontSize={'14px'}
/>
);
})}
</VStack>
<Flex
alignItems={'center'}
gap={'8px'}
px={'16px'}
py={'6px'}
height={'40px'}
cursor={'pointer'}
onClick={() => {
createTeamDisclosure.onOpen();
}}
>
<Plus size={20} color="#71717A" />
<Text fontSize="14px" fontWeight="400" color="#18181B">
{t('common:create_workspace')}
</Text>
</Flex>
<Divider my={'4px'} borderColor={'#F4F4F5'} />
{/* TeamCenter */}
<HStack
fontSize={'14px'}
px={'8px'}
alignItems={'center'}
cursor={'pointer'}
borderRadius={'4px'}
onClick={() => {
// setMessageFilter([]);
modalDisclosure.onOpen();
onClose();
}}
// {...props}
>
<Center p={'6px 8px'} gap={'12px'}>
<Settings size={16} color={'#737373'} />
<Text>{t('common:manage_team')}</Text>
</Center>
</HStack>
</PopoverBody>
</PopoverContent>
);
};
return (
<>
<Popover placement="bottom-start" isLazy>
{({ isOpen }) => (
{({ isOpen, onClose }) => (
<>
<PopoverTrigger>
<HStack
@@ -123,84 +225,7 @@ export default function WorkspaceToggle() {
</Center>
</HStack>
</PopoverTrigger>
<PopoverContent w={'full'} minW={'274px'}>
<PopoverBody
cursor={'initial'}
// maxH={'300px'}
// overflow={'auto'}
borderRadius={'12px'}
p={'0'}
py={'8px'}
color={'#18181B'}
style={{
scrollbarWidth: 'none'
}}
fontSize={'13px'}
>
<Text px={'12px'} py={'6px'} color={'#71717A'} fontSize={'12px'} fontWeight={'500'}>
{t('common:workspace')}
</Text>
<VStack gap={0} alignItems={'stretch'} maxH={'260px'} overflow={'scroll'}>
{namespaces.map((ns) => {
return (
<NsListItem
key={ns.uid}
width={'full'}
onClick={() => {
switchTeam({ uid: ns.uid });
}}
displayPoint={true}
id={ns.uid}
isPrivate={ns.nstype === NSType.Private}
isSelected={ns.uid === ns_uid}
teamName={ns.teamName}
teamAvatar={ns.id}
showCheck={true}
selectedColor={'rgba(0, 0, 0, 0.05)'}
fontSize={'14px'}
/>
);
})}
</VStack>
<Flex
alignItems={'center'}
gap={'8px'}
px={'16px'}
py={'6px'}
height={'40px'}
cursor={'pointer'}
onClick={() => {
createTeamDisclosure.onOpen();
}}
>
<Plus size={20} color="#71717A" />
<Text fontSize="14px" fontWeight="400" color="#18181B">
{t('common:create_workspace')}
</Text>
</Flex>
<Divider my={'4px'} borderColor={'#F4F4F5'} />
{/* TeamCenter */}
<HStack
fontSize={'14px'}
px={'8px'}
alignItems={'center'}
cursor={'pointer'}
borderRadius={'4px'}
onClick={() => {
// setMessageFilter([]);
modalDisclosure.onOpen();
}}
// {...props}
>
<Center p={'6px 8px'} gap={'12px'}>
<Settings size={16} color={'#737373'} />
<Text>{t('common:manage_team')}</Text>
</Center>
</HStack>
</PopoverBody>
</PopoverContent>
<WorkspaceList isOpen={isOpen} onClose={onClose} />
</>
)}
</Popover>
+17 -4
View File
@@ -2,9 +2,10 @@ import { getInviteCodeInfoRequest, reciveAction, verifyInviteCodeRequest } from
import useCallbackStore from '@/stores/callback';
import { useConfigStore } from '@/stores/config';
import useSessionStore from '@/stores/session';
import { ROLE_LIST } from '@/types/team';
import { ROLE_LIST, UserRole } from '@/types/team';
import { compareFirstLanguages } from '@/utils/tools';
import { Button, Flex, Image, Text, VStack } from '@chakra-ui/react';
import { track } from '@sealos/gtm';
import { dehydrate, QueryClient, useMutation, useQuery } from '@tanstack/react-query';
import { isString } from 'lodash';
import type { NextPage } from 'next';
@@ -21,9 +22,7 @@ const Callback: NextPage = () => {
const logo = useConfigStore().layoutConfig?.logo;
const { setWorkspaceInviteCode } = useCallbackStore();
const { t } = useTranslation();
const verifyMutation = useMutation({
mutationFn: verifyInviteCodeRequest
});
const inviteTips = ({ managerName, teamName, role }: Record<string, string>) =>
t('common:receive_tips', {
managerName,
@@ -44,6 +43,20 @@ const Callback: NextPage = () => {
}),
enabled: isString(inviteCode)
});
const verifyMutation = useMutation({
mutationFn: verifyInviteCodeRequest,
onSuccess: (_data, variables) => {
if (variables.action === reciveAction.Accepte) {
track('workspace_join', {
module: 'workspace',
trigger: 'invite',
role: infoResp?.data?.data!.role === UserRole.Manager ? 'manager' : 'developer'
});
}
}
});
const reset = () => {
setWorkspaceInviteCode();
router.replace('/');
+7 -1
View File
@@ -7,6 +7,7 @@ import { appWithTranslation, useTranslation } from 'next-i18next';
import type { AppProps } from 'next/app';
import Router from 'next/router';
import { useEffect } from 'react';
import { GTMScript } from '@sealos/gtm';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import '@sealos/driver/src/driver.css';
@@ -27,7 +28,7 @@ Router.events.on('routeChangeError', () => NProgress.done());
const App = ({ Component, pageProps }: AppProps) => {
const { i18n } = useTranslation();
const { initAppConfig } = useConfigStore();
const { initAppConfig, layoutConfig } = useConfigStore();
useEffect(() => {
initAppConfig();
@@ -40,6 +41,11 @@ const App = ({ Component, pageProps }: AppProps) => {
return (
<QueryClientProvider client={queryClient}>
<GTMScript
enabled={!!layoutConfig?.gtmId}
gtmId={layoutConfig?.gtmId ?? ''}
debug={process.env.NODE_ENV === 'development'}
/>
<Hydrate state={pageProps.dehydratedState}>
<ChakraProvider theme={theme}>
<Component {...pageProps} />
+17
View File
@@ -7,6 +7,7 @@ import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AppStateManager from '../utils/ProcessManager';
import { useDesktopConfigStore } from './desktopConfig';
import { track } from '@sealos/gtm';
export class AppInfo {
pid: number;
@@ -126,6 +127,8 @@ const useAppStore = create<TOSState>()(
},
openApp: async (app: TApp, { query, raw, pathname = '/', appSize = 'maximize' } = {}) => {
console.log('open app: ', app.key);
useDesktopConfigStore.getState().temporarilyDisableAnimation();
const zIndex = get().maxZIndex + 1;
// debugger
@@ -163,6 +166,19 @@ const useAppStore = create<TOSState>()(
state.currentAppPid = _app.pid;
state.maxZIndex = zIndex;
});
if (app.key.startsWith('system-')) {
track('module_open', {
module: app.key.slice(7)
});
} else {
track('app_launch', {
module: 'desktop',
app_name: app.name,
// All icons are from appstore as of now.
source: 'appstore'
});
}
},
// open desktop app by app key and pathname, and send message to app
openDesktopApp: ({
@@ -178,6 +194,7 @@ const useAppStore = create<TOSState>()(
pathname: string;
appSize?: WindowSize;
}) => {
console.log('open desktop app: ', appKey);
const app = get().installedApps.find((item) => item.key === appKey);
const runningApp = get().runningInfo.find((item) => item.key === appKey);
+3 -1
View File
@@ -83,6 +83,7 @@ export type LayoutConfigType = {
aiAssistantEnabled: boolean;
bannerEnabled: boolean;
};
gtmId: string | null;
};
export type AuthConfigType = {
@@ -317,7 +318,8 @@ export const DefaultLayoutConfig: LayoutConfigType = {
accountSettingEnabled: false,
aiAssistantEnabled: false,
bannerEnabled: false
}
},
gtmId: null
};
export const DefaultAuthClientConfig: AuthClientConfigType = {
+5 -6
View File
@@ -1,9 +1,14 @@
// Legacy GTM v1 events
/** @deprecated */
export const gtmLoginStart = () =>
window?.dataLayer?.push?.({
event: 'login_start',
module: 'auth',
context: 'app'
});
/** @deprecated */
export const gtmLoginSuccess = ({
method,
oauth2Provider,
@@ -21,9 +26,3 @@ export const gtmLoginSuccess = ({
module: 'auth',
context: 'app'
});
export const gtmOpenCostcenter = () =>
window?.dataLayer?.push({
event: 'module_open',
module: 'costcenter',
context: 'app'
});
@@ -56,7 +56,7 @@ export function GTMScript({ gtmId, enabled = true, debug = false, onInit }: GTMS
<>
<Script
id="gtm-script"
strategy="beforeInteractive"
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtm.js?id=${gtmId}`}
onLoad={handleScriptLoad}
onError={() => {
+6 -3
View File
@@ -20,9 +20,12 @@ class GTMTracker {
return this;
}
track(event: GTMEvent): void;
track<T extends GTMEventType>(eventType: T, properties?: EventProperties<T>): void;
track<T extends GTMEventType>(eventOrType: GTMEvent | T, properties?: EventProperties<T>): void {
track(event: Readonly<GTMEvent>): void;
track<T extends GTMEventType>(eventType: T, properties?: Readonly<EventProperties<T>>): void;
track<T extends GTMEventType>(
eventOrType: Readonly<GTMEvent> | T,
properties?: Readonly<EventProperties<T>>
): void {
if (!this.config.enabled) return;
let gtmEvent: GTMEvent;
+34 -9
View File
@@ -15,6 +15,8 @@ export type GTMModule =
| 'dashboard'
| 'auth';
export type GTMGuide = 'devbox' | 'database' | 'applaunchpad' | 'appstore';
export type GTMContext = 'app' | 'website';
export interface BaseGTMEvent {
@@ -24,9 +26,11 @@ export interface BaseGTMEvent {
method?: string;
}
export interface ModuleOpenEvent extends BaseGTMEvent {
export interface ModuleOpenEvent extends Omit<BaseGTMEvent, 'module'> {
event: 'module_open';
trigger?: 'manual' | 'onboarding';
// Module key on desktop is not constrained.
module: string;
}
export interface ModuleViewEvent extends BaseGTMEvent {
@@ -61,7 +65,7 @@ export interface DeploymentCreateEvent extends BaseGTMEvent {
replicas?: number;
storage?: number;
scaling?: {
method: 'CPU' | 'RAM';
method: 'CPU' | 'RAM' | 'GPU';
value: number;
};
};
@@ -70,8 +74,16 @@ export interface DeploymentCreateEvent extends BaseGTMEvent {
};
}
export interface DeploymentActionEvent extends BaseGTMEvent {
event: 'deployment_update' | 'deployment_delete' | 'deployment_details';
export interface DeploymentDetailsEvent extends BaseGTMEvent {
event: 'deployment_details';
}
export interface DeploymentUpdateEvent extends BaseGTMEvent {
event: 'deployment_update';
}
export interface DeploymentDeleteEvent extends BaseGTMEvent {
event: 'deployment_delete';
}
export interface DeploymentShutdownEvent extends BaseGTMEvent {
@@ -79,6 +91,15 @@ export interface DeploymentShutdownEvent extends BaseGTMEvent {
type: 'normal' | 'cost_saving';
}
export interface DeploymentRestartEvent extends BaseGTMEvent {
event: 'deployment_restart';
}
export interface DeploymentActionEvent extends BaseGTMEvent {
event: 'deployment_action';
event_type: 'terminal_open';
}
export interface IDEOpenEvent extends BaseGTMEvent {
event: 'ide_open';
module: 'devbox';
@@ -104,20 +125,20 @@ export interface ErrorOccurredEvent extends BaseGTMEvent {
export interface GuideStartEvent extends BaseGTMEvent {
event: 'guide_start';
module: 'guide';
guide_name: string;
guide_name: GTMGuide;
}
export interface GuideCompleteEvent extends BaseGTMEvent {
event: 'guide_complete';
module: 'guide';
guide_name: string;
guide_name: GTMGuide;
duration_seconds: number;
}
export interface GuideExitEvent extends BaseGTMEvent {
event: 'guide_exit';
module: 'guide';
guide_name?: string;
guide_name?: GTMGuide;
progress_step?: number;
duration_seconds?: number;
}
@@ -125,7 +146,7 @@ export interface GuideExitEvent extends BaseGTMEvent {
export interface AnnouncementClickEvent extends BaseGTMEvent {
event: 'announcement_click';
module: 'dashboard';
announcement_id: string;
announcement_id: 'invitation_referral_prompt' | 'onboarding_guide_prompt';
}
export interface WorkspaceCreateEvent extends BaseGTMEvent {
@@ -175,8 +196,12 @@ export type GTMEvent =
| AppLaunchEvent
| DeploymentStartEvent
| DeploymentCreateEvent
| DeploymentActionEvent
| DeploymentDetailsEvent
| DeploymentUpdateEvent
| DeploymentDeleteEvent
| DeploymentShutdownEvent
| DeploymentRestartEvent
| DeploymentActionEvent
| IDEOpenEvent
| ReleaseCreateEvent
| PaywallTriggeredEvent
+4 -1
View File
@@ -710,6 +710,9 @@ importers:
'@sealos/driver':
specifier: workspace:^
version: link:../../packages/driver
'@sealos/gtm':
specifier: workspace:^
version: link:../../packages/gtm
'@sealos/ui':
specifier: workspace:^
version: link:../../packages/ui
@@ -13134,7 +13137,7 @@ packages:
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2)
debug: 4.3.4
debug: 4.3.6
eslint: 8.38.0
typescript: 5.2.2
transitivePeerDependencies:
@@ -19,6 +19,7 @@ launchpad:
title: 'Sealos AppLaunchpad'
description: 'Generated by Sealos Team'
scripts: []
gtmId: null
pvcStorageMax: 100
eventAnalyze:
enabled: false
@@ -21,6 +21,7 @@
"@kubernetes/client-node": "^0.18.1",
"@next/font": "13.1.6",
"@sealos/driver": "workspace:^",
"@sealos/gtm": "workspace:^",
"@sealos/ui": "workspace:^",
"@tanstack/react-query": "^4.35.3",
"@tanstack/react-table": "^8.10.7",
+37 -5
View File
@@ -5,6 +5,7 @@ import type { AppDetailType, AppPatchPropsType, PodDetailType } from '@/types/ap
import { MonitorDataResult, MonitorQueryKey } from '@/types/monitor';
import { LogQueryPayload } from '@/pages/api/log/queryLogs';
import { PodListQueryPayload } from '@/pages/api/log/queryPodList';
import { track } from '@sealos/gtm';
export const postDeployApp = (yamlList: string[]) => POST('/api/applyApp', { yamlList });
@@ -12,12 +13,24 @@ export const putApp = (data: {
patch: AppPatchPropsType;
appName: string;
stateFulSetYaml?: string;
}) => POST('/api/updateApp', data);
}) => {
track('deployment_update', {
module: 'applaunchpad'
});
return POST('/api/updateApp', data);
};
export const getMyApps = () =>
GET<V1Deployment & V1StatefulSet[]>('/api/getApps').then((res) => res.map(adaptAppListItem));
export const delAppByName = (name: string) => DELETE('/api/delApp', { name });
export const delAppByName = (name: string) => {
track('deployment_delete', {
module: 'applaunchpad'
});
return DELETE('/api/delApp', { name });
};
export const getAppByName = (name: string, mock = false) =>
GET<AppDetailType>(`/api/getAppByAppName?appName=${name}&mock=${mock}`);
@@ -42,11 +55,30 @@ export const getPodLogs = (data: {
export const getPodEvents = (podName: string) =>
GET(`/api/getPodEvents?podName=${podName}`).then(adaptEvents);
export const restartAppByName = (appName: string) => GET(`/api/restartApp?appName=${appName}`);
export const restartAppByName = (appName: string) => {
track('deployment_restart', {
module: 'applaunchpad'
});
export const pauseAppByName = (appName: string) => GET(`/api/pauseApp?appName=${appName}`);
return GET(`/api/restartApp?appName=${appName}`);
};
export const startAppByName = (appName: string) => GET(`/api/startApp?appName=${appName}`);
export const pauseAppByName = (appName: string) => {
track('deployment_shutdown', {
module: 'applaunchpad',
type: 'normal'
});
return GET(`/api/pauseApp?appName=${appName}`);
};
export const startAppByName = (appName: string) => {
track('deployment_start', {
module: 'applaunchpad'
});
return GET(`/api/startApp?appName=${appName}`);
};
export const restartPodByName = (podName: string) => GET(`/api/restartPod?podName=${podName}`);
@@ -29,6 +29,7 @@ import React, { useCallback, useState } from 'react';
import { sealosApp } from 'sealos-desktop-sdk/app';
import { MOCK_APP_DETAIL } from '@/mock/apps';
import { useAppStore } from '@/store/app';
import { track } from '@sealos/gtm';
const LogsModal = dynamic(() => import('./LogsModal'));
const DetailModel = dynamic(() => import('./PodDetailModal'));
@@ -188,6 +189,10 @@ const Pods = ({ pods = [], appName }: { pods: PodDetailType[]; appName: string }
<Button
variant={'square'}
onClick={() => {
track('deployment_action', {
event_type: 'terminal_open',
module: 'applaunchpad'
});
const defaultCommand = `kubectl exec -it ${item.podName} -c ${appName} -- sh -c "clear; (bash || ash || sh)"`;
sealosApp.runEvents('openDesktopApp', {
appKey: 'system-terminal',
@@ -42,6 +42,7 @@ import { applistDriverObj, startDriver } from '@/hooks/driver';
import LangSelect from '../LangSelect';
import { useClientSideValue } from '@/hooks/useClientSideValue';
import { PencilLine } from 'lucide-react';
import { track } from '@sealos/gtm';
const DelModal = dynamic(() => import('@/components/app/detail/index/DelModal'));
@@ -550,7 +551,12 @@ const AppList = ({
w={'156px'}
flex={'0 0 auto'}
leftIcon={<MyIcon name={'plus'} w={'20px'} fill={'#FFF'} />}
onClick={() => router.push('/app/edit')}
onClick={() => {
track('deployment_start', {
module: 'applaunchpad'
});
router.push('/app/edit');
}}
>
{t('Create Application')}
</Button>
@@ -25,6 +25,7 @@ const fs = require('fs');
import * as yaml from 'js-yaml';
import type { AppConfigType } from '@/types';
import Script from 'next/script';
import { GTMScript } from '@sealos/gtm';
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
@@ -204,6 +205,11 @@ const MyApp = ({ Component, pageProps, config }: AppProps & AppOwnProps) => {
{config?.launchpad?.meta?.scripts?.map((script, i) => (
<Script key={i} {...script} />
))}
<GTMScript
enabled={!!config?.launchpad?.gtmId}
gtmId={config?.launchpad?.gtmId!}
debug={process.env.NODE_ENV === 'development'}
/>
</>
);
};
@@ -35,6 +35,7 @@ export const defaultAppConfig: AppConfigType = {
description: 'Sealos Desktop App Demo',
scripts: []
},
gtmId: null,
currencySymbol: Coin.shellCoin,
pvcStorageMax: 20,
eventAnalyze: {
@@ -6,11 +6,12 @@ import { serviceSideProps } from '@/utils/i18n';
import { Box, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import React from 'react';
import React, { useEffect } from 'react';
import AppBaseInfo from '@/components/app/detail/index/AppBaseInfo';
import Pods from '@/components/app/detail/index/Pods';
import DetailLayout from '@/components/layouts/DetailLayout';
import AdvancedInfo from '@/components/app/detail/index/AdvancedInfo';
import { track } from '@sealos/gtm';
const AppMainInfo = dynamic(() => import('@/components/app/detail/index/AppMainInfo'), {
ssr: false
@@ -32,6 +33,12 @@ const AppDetail = ({ appName }: { appName: string }) => {
}
);
useEffect(() => {
track('deployment_details', {
module: 'applaunchpad'
});
}, []);
return (
<DetailLayout appName={appName} key={'detail'}>
<Flex
@@ -18,6 +18,7 @@ import { downLoadBold } from '@/utils/tools';
import { useLogStore } from '@/store/logStore';
import { useRouter } from 'next/router';
import { useMessage } from '@sealos/ui';
import { track } from '@sealos/gtm';
export interface JsonFilterItem {
key: string;
@@ -50,6 +51,13 @@ export default function LogsPage({ appName }: { appName: string }) {
const { refreshInterval, setRefreshInterval, startDateTime, endDateTime } = useDateTimeStore();
const { setLogs, exportLogs, parsedLogs, logCounts, setLogCounts } = useLogStore();
useEffect(() => {
track('module_view', {
module: 'applaunchpad',
view_name: 'logs'
});
}, []);
const formHook = useForm<LogsFormData>({
defaultValues: {
pods: [],
@@ -12,6 +12,7 @@ import { ListItem } from '@/components/AdvancedSelect';
import useDateTimeStore from '@/store/date';
import { getAppMonitorData } from '@/api/app';
import EmptyChart from '@/components/Icon/icons/emptyChart.svg';
import { track } from '@sealos/gtm';
export default function MonitorPage({ appName }: { appName: string }) {
const { toast } = useToast();
@@ -21,6 +22,13 @@ export default function MonitorPage({ appName }: { appName: string }) {
const [podList, setPodList] = useState<ListItem[]>([]);
const { refreshInterval } = useDateTimeStore();
useEffect(() => {
track('module_view', {
module: 'applaunchpad',
view_name: 'monitors'
});
}, []);
useEffect(() => {
if (appDetailPods?.length > 0 && podList.length === 0) {
setPodList(
@@ -34,6 +34,7 @@ import { useMessage } from '@sealos/ui';
import { customAlphabet } from 'nanoid';
import { ResponseCode } from '@/types/response';
import { useGuideStore } from '@/store/guide';
import { track } from '@sealos/gtm';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 12);
@@ -195,12 +196,27 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
if (error?.code === ResponseCode.BALANCE_NOT_ENOUGH) {
setErrorMessage(t('user_balance_not_enough'));
setErrorCode(ResponseCode.BALANCE_NOT_ENOUGH);
track('paywall_triggered', {
module: 'applaunchpad',
type: 'insufficient_balance'
});
} else if (error?.code === ResponseCode.FORBIDDEN_CREATE_APP) {
setErrorMessage(t('forbidden_create_app'));
setErrorCode(ResponseCode.FORBIDDEN_CREATE_APP);
track('error_occurred', {
module: 'applaunchpad',
error_code: 'FORBIDDEN_CREATE_APP'
});
} else if (error?.code === ResponseCode.APP_ALREADY_EXISTS) {
setErrorMessage(t('app_already_exists'));
setErrorCode(ResponseCode.APP_ALREADY_EXISTS);
track('error_occurred', {
module: 'applaunchpad',
error_code: 'APP_ALREADY_EXISTS'
});
} else {
setErrorMessage(JSON.stringify(error));
}
@@ -409,7 +425,34 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
}
}
openConfirm(() => submitSuccess(parseYamls))();
openConfirm(() => {
track('deployment_create', {
module: 'applaunchpad',
method: 'custom',
config: {
template_type: 'public',
template_name: data.imageName,
template_version: data.imageName.split(':')?.[1] ?? 'latest'
},
resources: {
cpu_cores: data.cpu,
ram_mb: data.memory,
replicas: data.hpa.use ? data.hpa.maxReplicas : Number(data.replicas),
scaling: data.hpa.use
? {
method:
data.hpa.target === 'cpu'
? 'CPU'
: data.hpa.target === 'gpu'
? 'GPU'
: 'RAM',
value: data.hpa.value
}
: undefined
}
});
submitSuccess(parseYamls);
})();
}, submitError)();
}}
/>
+1
View File
@@ -54,6 +54,7 @@ export type AppConfigType = {
[key: string]: string;
}[];
};
gtmId: string | null;
currencySymbol: Coin;
pvcStorageMax: number;
eventAnalyze: {