mirror of
https://github.com/labring/sealos.git
synced 2026-08-31 02:06:00 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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,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} />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+4
-1
@@ -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",
|
||||
|
||||
@@ -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)();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -54,6 +54,7 @@ export type AppConfigType = {
|
||||
[key: string]: string;
|
||||
}[];
|
||||
};
|
||||
gtmId: string | null;
|
||||
currencySymbol: Coin;
|
||||
pvcStorageMax: number;
|
||||
eventAnalyze: {
|
||||
|
||||
Reference in New Issue
Block a user